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

Yannick Lecaillez
29.48.2016 9020a676bbe359cb158e96761ef6f1a3c32c80e5
opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/opendj-rest2ldap-config.json
@@ -3,7 +3,7 @@
    // Servlet and authentication filter.
    "ldapConnectionFactories" : {
        // Unauthenticated connections used for performing bind requests.
        "default" : {
        "bind" : {
            // Indicates whether or not LDAP connections should be secured using
            // SSL or StartTLS. Acceptable values are:
            //
@@ -57,7 +57,7 @@
        // authentication and proxied operations (if enabled). This factory
        // will re-use the server "default" configuration.
        "root" : {
            "inheritFrom"    : "default",
            "inheritFrom"    : "bind",
            // Defines how authentication should be performed. Only "simple"
            // authentication is supported at the moment.
@@ -70,201 +70,185 @@
        }
    },
    // The Rest2LDAP authentication filter configuration. The filter will be
    // disabled if the configuration is not present. Upon successful
    // authentication the filter will create a security context containing the
    // following principals:
    //
    // "dn" - the DN of the user if known (may not be the case for sasl-plain)
    // "id" - the username used for authentication.
    "authenticationFilter" : {
        // Indicates whether the filter should allow HTTP BASIC authentication.
        "supportHTTPBasicAuthentication" : true,
    "authorization": {
      // The authorization policies to use. Supported policies are "anonymous", "basic" and "oauth2".
        "policies": [ "basic" ],
        // Indicates whether the filter should allow alternative authentication
        // and, if so, which HTTP headers it should obtain the username and
        // password from.
        "supportAltAuthentication"        : true,
        "altAuthenticationUsernameHeader" : "X-OpenIDM-Username",
        "altAuthenticationPasswordHeader" : "X-OpenIDM-Password",
        // Perform all operations using a pre-authorization connection.
        "anonymous": {
            // Specify the connection factory to use to perform LDAP operations.
            // If missing, "root" factory will be used.
            "ldapConnectionFactory": "root",
            // Enable proxied authorization using the specified user DN.
            // If empty, anonymous proxied authorization will be used.
            // If missing, connection from the ldapConnectionFactory will be used as-is.
            "userDN": ""
        },
        // Indicates whether the authenticated LDAP connection should be cached
        // for use within the Rest2LDAP Servlet for subsequent LDAP operations.
        // If this is set to true then the Servlet will not need its own LDAP
        // connection factory and will also not need to use proxied
        // authorization.
        "reuseAuthenticatedConnection" : true,
      // Use HTTP Basic authentication's information to bind to the LDAP server.
      "basic": {
         // Indicates whether the filter should allow alternative authentication
         // and, if so, which HTTP headers it should obtain the username and
         // password from.
         "supportAltAuthentication"        : true,
         "altAuthenticationUsernameHeader" : "X-OpenIDM-Username",
         "altAuthenticationPasswordHeader" : "X-OpenIDM-Password",
        // Specifies how LDAP authentications should be performed. The method
        // must be one of:
        //
        // "simple"        - the username is an LDAP DN
        // "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
        //                   LDAP search using a filter constructed by
        //                   substituting the username into the
        //                   "searchFilterTemplate" using %s substitution.
        "method" : "search-simple",
         // For server which are not supporting proxied-authorization control, you can
         // set this flag to true. Subsequent LDAP operations will then be performed
         // by re-using the authenticated connection.
         // If missing, proxied-authorization control will be used.
         "reuseAuthenticatedConnection": false,
        // The connection factory which will be exclusively used for
        // authenticating users using LDAP bind operations.
        "bindLDAPConnectionFactory" : "default",
         // Define which LDAP bind mechanism to use
         // Supported mechanisms are "simple", "sasl-plain", "search"
         "bind": "search",
        // The SASL AuthzID template which will be used for "sasl-plain"
        // authentication. The %s format parameters will be substituted with
        // the client-provided username, using DN character escaping for DN
        // AuthzIDs.
        "saslAuthzIdTemplate" : "dn:uid=%s,ou=people,dc=example,dc=com",
         // Bind to the LDAP server using the DN built from the HTTP Basic's username
         "simple": {
            // Connection factory used to perform the bind operation.
            // If missing, "bind" factory will be used.
            "ldapConnectionFactory": "bind",
        // The connection factory which will be used for performing LDAP
        // searches to locate users when "search-simple" authentication is
        // enabled.
        "searchLDAPConnectionFactory" : "root",
            // The Bind DN Template containing a single %s which will be replaced by the authenticating
            // user's name. (i.e: uid=%s,ou=People,dc=example,dc=com)
            // If missing, "%s" is used.
            "bindDNTemplate": "uid=%s,ou=People,dc=example,dc=com"
         },
        // The search parameters to use for "search-simple" authentication. The
        // %s filter format parameters will be substituted with the
        // client-provided username, using LDAP filter string character escaping.
        "searchBaseDN"         : "ou=people,dc=example,dc=com",
        "searchScope"          : "sub", // Or "one".
        "searchFilterTemplate" : "(&(uid=%s)(objectClass=inetOrgPerson))"
         // Bind to the LDAP server using a SASL Plain request
         "sasl-plain": {
            // Connection factory used to perform the bind operation.
            // If missing, "bind" factory will be used.
             "ldapConnectionFactory": "bind",
        // TODO: support for HTTP sessions?
            // Authentication identity template containing a single %s which will be replaced by the authenticating
                // user's name. (i.e: u:%s)
            "authcIdTemplate": "u:%s"
         },
         // Bind to the LDAP server using the resulting DN of a search request.
         "search": {
            // Connection factory used to perform the search operation.
            // If missing, "root" factory will be used.
            "searchLDAPConnectionFactory": "root",
            // Connection factory used to perform the bind operation.
            // If missing, "bind" factory will be used.
            "bindLDAPConnectionFactory": "bind",
                // The %s filter format parameters will be substituted with the client-provided username,
                // using LDAP filter string character escaping.
                "baseDN"         : "ou=people,dc=example,dc=com",
                "scope"          : "sub", // Or "one".
                "filterTemplate" : "(&(uid=%s)(objectClass=inetOrgPerson))"
         }
         // TODO: support for HTTP sessions?
      }
    },
    // The Rest2LDAP Servlet configuration.
    "servlet" : {
        // The connection factory which will be used for performing LDAP
        // operations. Pre-authenticated connections passed through from the
        // authentication filter (see "reuseAuthenticatedConnection") will be
        // used in preference to this factory. Specifically, a connection
        // factory does not need to be configured if a connection will always
        // be passed on from the filter, which may not always be the case
        // if the filter is configured to use HTTP sessions.
        "ldapConnectionFactory" : "root",
        // Specifies how LDAP authorization should be performed. The method
        // must be one of:
        //
        // "none"        - use connections acquired from the LDAP connection
        //                 factory. Don't use proxied authorization, and don't
        //                 use cached pre-authenticated connections,
        // "reuse"       - use the connection obtained during LDAP
        //                 authentication. If no connection was passed through
        //                 the authorization will fail,
        // "proxy"       - use proxied authorization with an authorization ID
        //                 derived from the "proxyAuthzIdTemplate". Proxied
        //                 authorization will only be used if there is no
        //                 pre-authenticated connection available.
        "authorizationPolicy" : "proxy",
        // The AuthzID template which will be used for proxied authorization.
        // The template should contain fields which are expected to be found in
        // the security context create during authentication, e.g. "dn" and "id".
        "proxyAuthzIdTemplate" : "dn:{dn}",
        // The REST APIs and their LDAP attribute mappings.
        "mappings" : {
            "/users" : {
                "baseDN"              : "ou=people,dc=example,dc=com",
                "readOnUpdatePolicy"  : "controls",
                "useSubtreeDelete"    : false,
                "usePermissiveModify" : true,
                "etagAttribute"       : "etag",
                "namingStrategy"      : {
                    "strategy"    : "clientDNNaming",
                    "dnAttribute" : "uid"
                },
                "additionalLDAPAttributes" : [
                    {
                        "type" : "objectClass",
                        "values" : [
                            "top",
                            "person",
                            "organizationalPerson",
                            "inetOrgPerson"
                        ]
                    }
                ],
                "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",
                        "primaryKey"    : "uid",
                        "mapper"         : { "object" : {
                            "_id"         : { "simple"   : { "ldapAttribute" : "uid", "isSingleValued" : true, "isRequired" : true } },
                            "displayName" : { "simple"   : { "ldapAttribute" : "cn", "isSingleValued" : true, "writability" : "readOnlyDiscardWrites" } }
                        } }
                    } },
                    "groups"     : { "reference" : {
                        "ldapAttribute" : "isMemberOf",
                        "baseDN"        : "ou=groups,dc=example,dc=com",
                        "writability"   : "readOnly",
                        "primaryKey"    : "cn",
                        "mapper"        : { "object" : {
                            "_id"         : { "simple"   : { "ldapAttribute" : "cn", "isSingleValued" : true } }
                        } }
                    } },
                    "contactInformation" : { "object" : {
                        "telephoneNumber" : { "simple" : { "ldapAttribute" : "telephoneNumber", "isSingleValued" : true } },
                        "emailAddress"    : { "simple" : { "ldapAttribute" : "mail", "isSingleValued" : true } }
                    } },
                    "meta"        : { "object" : {
                        "created"      : { "simple" : { "ldapAttribute" : "createTimestamp", "isSingleValued" : true, "writability" : "readOnly" } },
                        "lastModified" : { "simple" : { "ldapAttribute" : "modifyTimestamp", "isSingleValued" : true, "writability" : "readOnly" } }
                    } }
                }
            },
            "/groups" : {
                "baseDN"              : "ou=groups,dc=example,dc=com",
                "readOnUpdatePolicy"  : "controls",
                "useSubtreeDelete"    : false,
                "usePermissiveModify" : true,
                "etagAttribute"       : "etag",
                "namingStrategy"      : {
                    "strategy"    : "clientDNNaming",
                    "dnAttribute" : "cn"
                },
                "additionalLDAPAttributes" : [
                    {
                        "type" : "objectClass",
                        "values" : [
                            "top",
                            "groupOfUniqueNames"
                        ]
                    }
                ],
                "attributes" : {
                    "schemas"     : { "constant" : [ "urn:scim:schemas:core:1.0" ] },
                    "_id"         : { "simple"   : { "ldapAttribute" : "cn", "isSingleValued" : true, "isRequired" : true, "writability" : "createOnly" } },
                    "_rev"        : { "simple"   : { "ldapAttribute" : "etag", "isSingleValued" : true, "writability" : "readOnly" } },
                    "displayName" : { "simple"   : { "ldapAttribute" : "cn", "isSingleValued" : true, "isRequired" : true, "writability" : "readOnly" } },
                    "members"    : { "reference" : {
                        "ldapAttribute" : "uniqueMember",
                        "baseDN"        : "dc=example,dc=com",
                        "primaryKey"    : "uid",
                        "mapper"        : { "object" : {
                            "_id"         : { "simple"   : { "ldapAttribute" : "uid", "isSingleValued" : true, "isRequired" : true } },
                            "displayName" : { "simple"   : { "ldapAttribute" : "cn", "isSingleValued" : true, "writability" : "readOnlyDiscardWrites" } }
                        } }
                    } },
                    "meta"        : { "object" : {
                        "created"      : { "simple" : { "ldapAttribute" : "createTimestamp", "isSingleValued" : true, "writability" : "readOnly" } },
                        "lastModified" : { "simple" : { "ldapAttribute" : "modifyTimestamp", "isSingleValued" : true, "writability" : "readOnly" } }
                    } }
                }
            }
        }
    }
   // The REST APIs and their LDAP attribute mappings.
   "mappings" : {
       "/users" : {
         "baseDN"              : "ou=people,dc=example,dc=com",
         "readOnUpdatePolicy"  : "controls",
         "useSubtreeDelete"    : false,
         "usePermissiveModify" : true,
         "etagAttribute"       : "etag",
         "namingStrategy"      : {
             "strategy"    : "clientDNNaming",
             "dnAttribute" : "uid"
         },
         "additionalLDAPAttributes" : [
             {
               "type" : "objectClass",
               "values" : [
                   "top",
                   "person",
                   "organizationalPerson",
                  "inetOrgPerson"
               ]
             }
         ],
         "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",
               "primaryKey"    : "uid",
               "mapper"         : { "object" : {
                   "_id"         : { "simple"   : { "ldapAttribute" : "uid", "isSingleValued" : true, "isRequired" : true } },
                   "displayName" : { "simple"   : { "ldapAttribute" : "cn", "isSingleValued" : true, "writability" : "readOnlyDiscardWrites" } }
               } }
             } },
             "groups"     : { "reference" : {
               "ldapAttribute" : "isMemberOf",
               "baseDN"        : "ou=groups,dc=example,dc=com",
               "writability"   : "readOnly",
               "primaryKey"    : "cn",
               "mapper"        : { "object" : {
                   "_id"         : { "simple"   : { "ldapAttribute" : "cn", "isSingleValued" : true } }
               } }
             } },
             "contactInformation" : { "object" : {
               "telephoneNumber" : { "simple" : { "ldapAttribute" : "telephoneNumber", "isSingleValued" : true } },
               "emailAddress"    : { "simple" : { "ldapAttribute" : "mail", "isSingleValued" : true } }
             } },
             "meta"        : { "object" : {
               "created"      : { "simple" : { "ldapAttribute" : "createTimestamp", "isSingleValued" : true, "writability" : "readOnly" } },
               "lastModified" : { "simple" : { "ldapAttribute" : "modifyTimestamp", "isSingleValued" : true, "writability" : "readOnly" } }
             } }
         }
       },
       "/groups" : {
         "baseDN"              : "ou=groups,dc=example,dc=com",
         "readOnUpdatePolicy"  : "controls",
         "useSubtreeDelete"    : false,
         "usePermissiveModify" : true,
         "etagAttribute"       : "etag",
         "namingStrategy"      : {
            "strategy"    : "clientDNNaming",
            "dnAttribute" : "cn"
         },
         "additionalLDAPAttributes" : [
            {
               "type" : "objectClass",
               "values" : [
                  "top",
                  "groupOfUniqueNames"
               ]
            }
         ],
         "attributes" : {
            "schemas"     : { "constant" : [ "urn:scim:schemas:core:1.0" ] },
            "_id"         : { "simple"   : { "ldapAttribute" : "cn", "isSingleValued" : true, "isRequired" : true, "writability" : "createOnly" } },
            "_rev"        : { "simple"   : { "ldapAttribute" : "etag", "isSingleValued" : true, "writability" : "readOnly" } },
            "displayName" : { "simple"   : { "ldapAttribute" : "cn", "isSingleValued" : true, "isRequired" : true, "writability" : "readOnly" } },
            "members"    : { "reference" : {
               "ldapAttribute" : "uniqueMember",
               "baseDN"        : "dc=example,dc=com",
               "primaryKey"    : "uid",
               "mapper"        : { "object" : {
                  "_id"         : { "simple"   : { "ldapAttribute" : "uid", "isSingleValued" : true, "isRequired" : true } },
                  "displayName" : { "simple"   : { "ldapAttribute" : "cn", "isSingleValued" : true, "writability" : "readOnlyDiscardWrites" } }
               } }
            } },
            "meta"        : { "object" : {
               "created"      : { "simple" : { "ldapAttribute" : "createTimestamp", "isSingleValued" : true, "writability" : "readOnly" } },
               "lastModified" : { "simple" : { "ldapAttribute" : "modifyTimestamp", "isSingleValued" : true, "writability" : "readOnly" } }
            } }
         }
       }
   }
}
opendj-rest2ldap/pom.xml
@@ -22,7 +22,7 @@
        <groupId>org.forgerock.opendj</groupId>
        <version>4.0.0-SNAPSHOT</version>
    </parent>
    <artifactId>opendj-rest2ldap</artifactId>
    <name>OpenDJ Commons REST Adapter</name>
    <description>This module includes APIs for accessing LDAP repositories using commons REST.</description>
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLDAPAttributeMapper.java
@@ -11,7 +11,7 @@
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2013-2015 ForgeRock AS.
 * Copyright 2013-2016 ForgeRock AS.
 */
package org.forgerock.opendj.rest2ldap;
@@ -36,6 +36,7 @@
import org.forgerock.json.resource.ResourceException;
import org.forgerock.opendj.ldap.Attribute;
import org.forgerock.opendj.ldap.AttributeDescription;
import org.forgerock.opendj.ldap.Connection;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.LinkedAttribute;
import org.forgerock.opendj.ldap.Modification;
@@ -100,8 +101,8 @@
    @Override
    Promise<List<Attribute>, ResourceException> create(
            final RequestState requestState, final JsonPointer path, final JsonValue v) {
        return getNewLDAPAttributes(requestState, path, v).then(
            final Connection connection, final JsonPointer path, final JsonValue v) {
        return getNewLDAPAttributes(connection, path, v).then(
            new Function<Attribute, List<Attribute>, ResourceException>() {
                @Override
                public List<Attribute> apply(Attribute newLDAPAttribute) throws ResourceException {
@@ -125,19 +126,19 @@
    }
    @Override
    void getLDAPAttributes(final RequestState requestState, final JsonPointer path,
    void getLDAPAttributes(final Connection connection, final JsonPointer path,
            final JsonPointer subPath, final Set<String> ldapAttributes) {
        ldapAttributes.add(ldapAttributeName.toString());
    }
    abstract Promise<Attribute, ResourceException> getNewLDAPAttributes(
                RequestState requestState, JsonPointer path, List<Object> newValues);
    abstract Promise<Attribute, ResourceException> getNewLDAPAttributes(Connection connection, JsonPointer path,
            List<Object> newValues);
    abstract T getThis();
    @Override
    Promise<List<Modification>, ResourceException> patch(
                final RequestState requestState, final JsonPointer path, final PatchOperation operation) {
                final Connection connection, final JsonPointer path, final PatchOperation operation) {
        try {
            final JsonPointer field = operation.getField();
            final JsonValue v = operation.getValue();
@@ -262,7 +263,7 @@
                        singletonList(new Modification(modType, emptyAttribute(ldapAttributeName))));
                }
            } else {
                return getNewLDAPAttributes(requestState, path, newValues)
                return getNewLDAPAttributes(connection, path, newValues)
                        .then(new Function<Attribute, List<Modification>, ResourceException>() {
                            @Override
                            public List<Modification> apply(final Attribute value) {
@@ -278,9 +279,9 @@
    }
    @Override
    Promise<List<Modification>, ResourceException> update(
            final RequestState requestState, final JsonPointer path, final Entry e, final JsonValue v) {
        return getNewLDAPAttributes(requestState, path, v).then(
    Promise<List<Modification>, ResourceException> update(final Connection connection, final JsonPointer path,
            final Entry e, final JsonValue v) {
        return getNewLDAPAttributes(connection, path, v).then(
            new Function<Attribute, List<Modification>, ResourceException>() {
                @Override
                public List<Modification> apply(final Attribute newLDAPAttribute) throws ResourceException {
@@ -370,8 +371,8 @@
        }
    }
    private Promise<Attribute, ResourceException> getNewLDAPAttributes(
            final RequestState requestState, final JsonPointer path, final JsonValue v) {
    private Promise<Attribute, ResourceException> getNewLDAPAttributes(final Connection connection,
            final JsonPointer path, final JsonValue v) {
        try {
            // Ensure that the value is of the correct type.
            checkSchema(path, v);
@@ -380,7 +381,7 @@
                // Skip sub-class implementation if there are no values.
                return Promises.newResultPromise(emptyAttribute(ldapAttributeName));
            } else {
                return getNewLDAPAttributes(requestState, path, newValues);
                return getNewLDAPAttributes(connection, path, newValues);
            }
        } catch (final Exception ex) {
            return Promises.newExceptionPromise(asResourceException(ex));
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AttributeMapper.java
@@ -11,7 +11,7 @@
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2012-2015 ForgeRock AS.
 * Copyright 2012-2016 ForgeRock AS.
 */
package org.forgerock.opendj.rest2ldap;
@@ -23,6 +23,7 @@
import org.forgerock.json.resource.PatchOperation;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.opendj.ldap.Attribute;
import org.forgerock.opendj.ldap.Connection;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.Modification;
@@ -50,8 +51,8 @@
     * action in this case, perhaps by substituting default LDAP values, or by
     * returning a failed promise with an appropriate {@link ResourceException}.
     *
     * @param requestState
     *            The request state.
     * @param connection
     *            The LDAP connection to use to perform the operation.
     * @param path
     *            The pointer from the root of the JSON resource to this
     *            attribute mapper. This may be used when constructing error
@@ -62,8 +63,7 @@
     *            in the resource.
     * @return A {@link Promise} containing the result of the operation.
     */
    abstract Promise<List<Attribute>, ResourceException> create(
            RequestState requestState, JsonPointer path, JsonValue v);
    abstract Promise<List<Attribute>, ResourceException> create(Connection connection, JsonPointer path, JsonValue v);
    /**
     * Adds the names of the LDAP attributes required by this attribute mapper
@@ -72,8 +72,8 @@
     * Implementations should only add the names of attributes found in the LDAP
     * entry directly associated with the resource.
     *
     * @param requestState
     *            The request state.
     * @param connection
     *            The LDAP connection to use to perform the operation.
     * @param path
     *            The pointer from the root of the JSON resource to this
     *            attribute mapper. This may be used when constructing error
@@ -86,8 +86,8 @@
     *            The set into which the required LDAP attribute names should be
     *            put.
     */
    abstract void getLDAPAttributes(
            RequestState requestState, JsonPointer path, JsonPointer subPath, Set<String> ldapAttributes);
    abstract void getLDAPAttributes(Connection connection, JsonPointer path, JsonPointer subPath,
            Set<String> ldapAttributes);
    /**
     * Transforms the provided REST comparison filter parameters to an LDAP
@@ -98,8 +98,8 @@
     * promise must be returned with an appropriate {@link ResourceException}
     * indicating the problem which occurred.
     *
     * @param requestState
     *            The request state.
     * @param connection
     *            The LDAP connection to use to perform the operation.
     * @param path
     *            The pointer from the root of the JSON resource to this
     *            attribute mapper. This may be used when constructing error
@@ -119,7 +119,7 @@
     *            {@link FilterType#PRESENT}.
     * @return A {@link Promise} containing the result of the operation.
     */
    abstract Promise<Filter, ResourceException> getLDAPFilter(RequestState requestState, JsonPointer path,
    abstract Promise<Filter, ResourceException> getLDAPFilter(Connection connection, JsonPointer path,
            JsonPointer subPath, FilterType type, String operator, Object valueAssertion);
    /**
@@ -127,8 +127,8 @@
     * a promise once the transformation has completed. This method is invoked
     * when a REST resource is modified using a patch request.
     *
     * @param requestState
     *            The request state.
     * @param connection
     *            The LDAP connection to use to perform the operation.
     * @param path
     *            The pointer from the root of the JSON resource to this
     *            attribute mapper. This may be used when constructing error
@@ -141,7 +141,7 @@
     * @return A {@link Promise} containing the result of the operation.
     */
    abstract Promise<List<Modification>, ResourceException> patch(
            RequestState requestState, JsonPointer path, PatchOperation operation);
            Connection connection, JsonPointer path, PatchOperation operation);
    /**
     * Maps one or more LDAP attributes to their JSON representation, returning
@@ -158,8 +158,8 @@
     * they contain unexpected content, then a failed promise must be returned
     * with an appropriate exception indicating the problem which occurred.
     *
     * @param requestState
     *            The request state.
     * @param connection
     *            The LDAP connection to use to perform the operation.
     * @param path
     *            The pointer from the root of the JSON resource to this
     *            attribute mapper. This may be used when constructing error
@@ -168,7 +168,7 @@
     *            The LDAP entry to be converted to JSON.
     * @return A {@link Promise} containing the result of the operation.
     */
    abstract Promise<JsonValue, ResourceException> read(RequestState requestState, JsonPointer path, Entry e);
    abstract Promise<JsonValue, ResourceException> read(Connection connection, JsonPointer path, Entry e);
    /**
     * Maps a JSON value to one or more LDAP modifications, returning a promise
@@ -181,16 +181,16 @@
     * action in this case, perhaps by substituting default LDAP values, or by
     * returning a failed promise with an appropriate {@link ResourceException}.
     *
     * @param requestState
     *            The request state.
     * @param connection
     *            The LDAP connection to use to perform the operation.
     * @param v
     *            The JSON value to be converted to LDAP attributes, which may
     *            be {@code null} indicating that the JSON value was not present
     *            in the resource.
     * @return A {@link Promise} containing the result of the operation.
     */
    abstract Promise<List<Modification>, ResourceException> update(
            RequestState requestState, JsonPointer path, Entry e, JsonValue v);
    abstract Promise<List<Modification>, ResourceException> update(Connection connection, JsonPointer path, Entry e,
            JsonValue v);
    // TODO: methods for obtaining schema information (e.g. name, description, type information).
    // TODO: methods for creating sort controls.
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthenticatedConnectionContext.java
@@ -11,7 +11,7 @@
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2013-2015 ForgeRock AS.
 * Copyright 2013-2016 ForgeRock AS.
 */
package org.forgerock.opendj.rest2ldap;
@@ -78,7 +78,7 @@
     * @return The cached pre-authenticated LDAP connection which should be
     *         re-used for subsequent LDAP operations.
     */
    Connection getConnection() {
    public Connection getConnection() {
        return connection;
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthorizationPolicy.java
File was deleted
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Config.java
@@ -11,11 +11,10 @@
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2013 ForgeRock AS.
 * Copyright 2013-2016 ForgeRock AS.
 */
package org.forgerock.opendj.rest2ldap;
import org.forgerock.opendj.ldap.ConnectionFactory;
import org.forgerock.opendj.ldap.DecodeOptions;
import org.forgerock.opendj.ldap.schema.Schema;
@@ -23,40 +22,20 @@
 * Common configuration options.
 */
final class Config {
    private final AuthorizationPolicy authzPolicy;
    private final ConnectionFactory factory;
    private final DecodeOptions options;
    private final AuthzIdTemplate proxiedAuthzTemplate;
    private final ReadOnUpdatePolicy readOnUpdatePolicy;
    private final Schema schema;
    private final boolean useSubtreeDelete;
    private final boolean usePermissiveModify;
    Config(final ConnectionFactory factory, final ReadOnUpdatePolicy readOnUpdatePolicy,
            final AuthorizationPolicy authzPolicy, final AuthzIdTemplate proxiedAuthzTemplate,
            final boolean useSubtreeDelete, final boolean usePermissiveModify, final Schema schema) {
        this.factory = factory;
    Config(final ReadOnUpdatePolicy readOnUpdatePolicy, final boolean useSubtreeDelete,
            final boolean usePermissiveModify, final Schema schema) {
        this.readOnUpdatePolicy = readOnUpdatePolicy;
        this.authzPolicy = authzPolicy;
        this.proxiedAuthzTemplate = proxiedAuthzTemplate;
        this.useSubtreeDelete = useSubtreeDelete;
        this.usePermissiveModify = usePermissiveModify;
        this.schema = schema;
        this.options = new DecodeOptions().setSchema(schema);
    }
    /**
     * Returns the LDAP SDK connection factory which should be used when
     * performing LDAP operations.
     *
     * @return The LDAP SDK connection factory which should be used when
     *         performing LDAP operations.
     */
    ConnectionFactory connectionFactory() {
        return factory;
    }
    /**
     * Returns the decoding options which should be used when decoding controls
     * in responses.
     *
@@ -68,28 +47,6 @@
    }
    /**
     * Returns the authorization policy which should be used for performing LDAP
     * operations.
     *
     * @return The authorization policy which should be used for performing LDAP
     *         operations.
     */
    AuthorizationPolicy getAuthorizationPolicy() {
        return authzPolicy;
    }
    /**
     * Returns the authorization ID template which should be used when proxied
     * authorization is enabled.
     *
     * @return The authorization ID template which should be used when proxied
     *         authorization is enabled.
     */
    AuthzIdTemplate getProxiedAuthorizationTemplate() {
        return proxiedAuthzTemplate;
    }
    /**
     * Returns {@code true} if modify requests should include the permissive
     * modify control.
     *
@@ -121,15 +78,4 @@
    ReadOnUpdatePolicy readOnUpdatePolicy() {
        return readOnUpdatePolicy;
    }
    /**
     * Returns the schema which should be used when attribute types and
     * controls.
     *
     * @return The schema which should be used when attribute types and
     *         controls.
     */
    Schema schema() {
        return schema;
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/HttpAuthenticationFilter.java
File was deleted
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java
@@ -11,7 +11,7 @@
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2012-2015 ForgeRock AS.
 * Copyright 2012-2016 ForgeRock AS.
 */
package org.forgerock.opendj.rest2ldap;
@@ -32,6 +32,7 @@
import org.forgerock.json.resource.PatchOperation;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.opendj.ldap.Attribute;
import org.forgerock.opendj.ldap.Connection;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.Modification;
@@ -54,8 +55,8 @@
    }
    @Override
    Promise<List<Attribute>, ResourceException> create(
            final RequestState requestState, final JsonPointer path, final JsonValue v) {
    Promise<List<Attribute>, ResourceException> create(final Connection connection, final JsonPointer path,
            final JsonValue v) {
        if (!isNullOrEmpty(v) && !v.getObject().equals(value.getObject())) {
            return Promises.<List<Attribute>, ResourceException> newExceptionPromise(new BadRequestException(i18n(
                    "The request cannot be processed because it attempts to create the read-only field '%s'", path)));
@@ -65,13 +66,13 @@
    }
    @Override
    void getLDAPAttributes(final RequestState requestState, final JsonPointer path, final JsonPointer subPath,
    void getLDAPAttributes(final Connection connection, final JsonPointer path, final JsonPointer subPath,
            final Set<String> ldapAttributes) {
        // Nothing to do.
    }
    @Override
    Promise<Filter, ResourceException> getLDAPFilter(final RequestState requestState, final JsonPointer path,
    Promise<Filter, ResourceException> getLDAPFilter(final Connection connection, final JsonPointer path,
            final JsonPointer subPath, final FilterType type, final String operator, final Object valueAssertion) {
        final Filter filter;
        final JsonValue subValue = value.get(subPath);
@@ -109,20 +110,20 @@
    }
    @Override
    Promise<List<Modification>, ResourceException> patch(final RequestState requestState, final JsonPointer path,
    Promise<List<Modification>, ResourceException> patch(final Connection connection, final JsonPointer path,
            final PatchOperation operation) {
        return Promises.<List<Modification>, ResourceException> newExceptionPromise(new BadRequestException(i18n(
                "The request cannot be processed because it attempts to patch the read-only field '%s'", path)));
    }
    @Override
    Promise<JsonValue, ResourceException> read(final RequestState requestState, final JsonPointer path, final Entry e) {
    Promise<JsonValue, ResourceException> read(final Connection connection, final JsonPointer path, final Entry e) {
        return Promises.newResultPromise(value.copy());
    }
    @Override
    Promise<List<Modification>, ResourceException> update(
            final RequestState requestState, final JsonPointer path, final Entry e, final JsonValue v) {
            final Connection connection, final JsonPointer path, final Entry e, final JsonValue v) {
        if (!isNullOrEmpty(v) && !v.getObject().equals(value.getObject())) {
            return Promises.<List<Modification>, ResourceException> newExceptionPromise(new BadRequestException(i18n(
                    "The request cannot be processed because it attempts to modify the read-only field '%s'", path)));
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java
@@ -11,7 +11,7 @@
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2012-2015 ForgeRock AS.
 * Copyright 2012-2016 ForgeRock AS.
 */
package org.forgerock.opendj.rest2ldap;
@@ -36,7 +36,6 @@
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import org.forgerock.json.JsonPointer;
import org.forgerock.json.JsonValue;
@@ -174,47 +173,35 @@
                    ResourceException.newResourceException(ResourceException.BAD_REQUEST, e.getLocalizedMessage(), e));
        }
        final RequestState requestState = wrap(context);
        return requestState.getConnection()
                .thenAsync(new AsyncFunction<Connection, ActionResponse, ResourceException>() {
                    @Override
                    public Promise<ActionResponse, ResourceException> apply(final Connection connection)
                            throws ResourceException {
                        List<JsonPointer> attrs = Collections.emptyList();
                        return connection.searchSingleEntryAsync(searchRequest(requestState, resourceId, attrs))
                                .thenAsync(new AsyncFunction<SearchResultEntry, ActionResponse, ResourceException>() {
                                    @Override
                                    public Promise<ActionResponse, ResourceException> apply(
                                              final SearchResultEntry entry) {
                                        PasswordModifyExtendedRequest pwdModifyRequest =
                                                Requests.newPasswordModifyExtendedRequest();
                                        pwdModifyRequest.setUserIdentity("dn: " + entry.getName());
                                        pwdModifyRequest.setOldPassword(asBytes(oldPassword));
                                        pwdModifyRequest.setNewPassword(asBytes(newPassword));
                                        return connection.extendedRequestAsync(pwdModifyRequest)
                                            .thenAsync(new AsyncFunction<PasswordModifyExtendedResult,
                                                    ActionResponse, ResourceException>() {
                                                @Override
                                                public Promise<ActionResponse, ResourceException> apply(
                                                        PasswordModifyExtendedResult value) throws ResourceException {
                                                    JsonValue result = new JsonValue(new LinkedHashMap<>());
                                                    byte[] generatedPwd = value.getGeneratedPassword();
                                                    if (generatedPwd != null) {
                                                        result = result.put("generatedPassword",
                                                                ByteString.valueOfBytes(generatedPwd).toString());
                                                    }
                                                    return Responses.newActionResponse(result).asPromise();
                                                }
                                            }, ldapExceptionToResourceException());
                                    }
                                }, ldapExceptionToResourceException());
                    }
                    private AsyncFunction<LdapException, ActionResponse, ResourceException>
                    ldapExceptionToResourceException() {
                        return ldapToResourceException();
                    }
                }).thenFinally(close(requestState));
        final Connection connection = context.asContext(AuthenticatedConnectionContext.class).getConnection();
        List<JsonPointer> attrs = Collections.emptyList();
        return connection.searchSingleEntryAsync(searchRequest(connection, resourceId, attrs))
                         .thenAsync(new AsyncFunction<SearchResultEntry, ActionResponse, ResourceException>() {
                             @Override
                             public Promise<ActionResponse, ResourceException> apply(
                                       final SearchResultEntry entry) {
                                 PasswordModifyExtendedRequest pwdModifyRequest =
                                         Requests.newPasswordModifyExtendedRequest();
                                 pwdModifyRequest.setUserIdentity("dn: " + entry.getName());
                                 pwdModifyRequest.setOldPassword(asBytes(oldPassword));
                                 pwdModifyRequest.setNewPassword(asBytes(newPassword));
                                 return connection.extendedRequestAsync(pwdModifyRequest)
                                     .thenAsync(new AsyncFunction<PasswordModifyExtendedResult,
                                             ActionResponse, ResourceException>() {
                                         @Override
                                         public Promise<ActionResponse, ResourceException> apply(
                                                 PasswordModifyExtendedResult value) throws ResourceException {
                                             JsonValue result = new JsonValue(new LinkedHashMap<>());
                                             byte[] generatedPwd = value.getGeneratedPassword();
                                             if (generatedPwd != null) {
                                                 result = result.put("generatedPassword",
                                                         ByteString.valueOfBytes(generatedPwd).toString());
                                             }
                                             return Responses.newActionResponse(result).asPromise();
                                         }
                                     }, Exceptions.<ActionResponse>toResourceException());
                             }
                         }, Exceptions.<ActionResponse>toResourceException());
    }
    private byte[] asBytes(final String s) {
@@ -222,93 +209,80 @@
    }
    @Override
    public Promise<ResourceResponse, ResourceException> createInstance(
            final Context context, final CreateRequest request) {
        final RequestState requestState = wrap(context);
        return requestState.getConnection().thenAsync(
            new AsyncFunction<Connection, ResourceResponse, ResourceException>() {
                @Override
                public Promise<ResourceResponse, ResourceException> apply(final Connection connection)
                        throws ResourceException {
                    // Calculate entry content.
                    return attributeMapper.create(requestState, new JsonPointer(), request.getContent())
                            .thenAsync(new AsyncFunction<List<Attribute>, ResourceResponse, ResourceException>() {
                                @Override
                                public Promise<ResourceResponse, ResourceException> apply(
                                        final List<Attribute> attributes) {
                                    // Perform add operation.
                                    final AddRequest addRequest = newAddRequest(DN.rootDN());
                                    for (final Attribute attribute : additionalLDAPAttributes) {
                                        addRequest.addAttribute(attribute);
                                    }
                                    for (final Attribute attribute : attributes) {
                                        addRequest.addAttribute(attribute);
                                    }
                                    try {
                                        nameStrategy.setResourceId(requestState, getBaseDN(),
                                                request.getNewResourceId(), addRequest);
                                    } catch (final ResourceException e) {
                                        return Promises.newExceptionPromise(e);
                                    }
                                    if (config.readOnUpdatePolicy() == CONTROLS) {
                                        addRequest.addControl(PostReadRequestControl.newControl(
                                                false, getLDAPAttributes(requestState, request.getFields())));
                                    }
                                    return connection.applyChangeAsync(addRequest)
                                                     .thenAsync(postUpdateResultAsyncFunction(requestState),
                                                                ldapExceptionToResourceException());
                                }
                            });
                }
            }).thenFinally(close(requestState));
    public Promise<ResourceResponse, ResourceException> createInstance(final Context context,
            final CreateRequest request) {
        final Connection connection = context.asContext(AuthenticatedConnectionContext.class).getConnection();
        // Calculate entry content.
        return attributeMapper
                .create(connection, new JsonPointer(), request.getContent())
                .thenAsync(new AsyncFunction<List<Attribute>, ResourceResponse, ResourceException>() {
                    @Override
                    public Promise<ResourceResponse, ResourceException> apply(final List<Attribute> attributes) {
                        // Perform add operation.
                        final AddRequest addRequest = newAddRequest(DN.rootDN());
                        for (final Attribute attribute : additionalLDAPAttributes) {
                            addRequest.addAttribute(attribute);
                        }
                        for (final Attribute attribute : attributes) {
                            addRequest.addAttribute(attribute);
                        }
                        try {
                            nameStrategy.setResourceId(connection, getBaseDN(),
                                    request.getNewResourceId(),
                                    addRequest);
                        } catch (final ResourceException e) {
                            return Promises.newExceptionPromise(e);
                        }
                        if (config.readOnUpdatePolicy() == CONTROLS) {
                            addRequest.addControl(PostReadRequestControl.newControl(false,
                                    getLDAPAttributes(connection, request.getFields())));
                        }
                        return connection.applyChangeAsync(addRequest)
                                         .thenAsync(
                                                 postUpdateResultAsyncFunction(connection),
                                                 Exceptions.<ResourceResponse>toResourceException());
                    }
                });
    }
    @Override
    public Promise<ResourceResponse, ResourceException> deleteInstance(
            final Context context, final String resourceId, final DeleteRequest request) {
        final RequestState requestState = wrap(context);
        final AtomicReference<Connection> connectionHolder = new AtomicReference<>();
        return requestState.getConnection()
                .thenOnResult(saveConnection(connectionHolder))
                .thenAsync(doUpdateFunction(requestState, resourceId, request.getRevision()))
        final Connection connection = context.asContext(AuthenticatedConnectionContext.class).getConnection();
        return doUpdateFunction(connection, resourceId, request.getRevision())
                .thenAsync(new AsyncFunction<DN, ResourceResponse, ResourceException>() {
                    @Override
                    public Promise<ResourceResponse, ResourceException> apply(DN dn) throws ResourceException {
                        try {
                            final ChangeRecord deleteRequest = newDeleteRequest(dn);
                            if (config.readOnUpdatePolicy() == CONTROLS) {
                                final String[] attributes = getLDAPAttributes(requestState, request.getFields());
                                final String[] attributes = getLDAPAttributes(connection, request.getFields());
                                deleteRequest.addControl(PreReadRequestControl.newControl(false, attributes));
                            }
                            if (config.useSubtreeDelete()) {
                                deleteRequest.addControl(SubtreeDeleteRequestControl.newControl(true));
                            }
                            addAssertionControl(deleteRequest, request.getRevision());
                            return connectionHolder.get().applyChangeAsync(deleteRequest)
                                                         .thenAsync(postUpdateResultAsyncFunction(requestState),
                                                                    ldapExceptionToResourceException());
                            return connection.applyChangeAsync(deleteRequest)
                                             .thenAsync(
                                                     postUpdateResultAsyncFunction(connection),
                                                     Exceptions.<ResourceResponse>toResourceException());
                        } catch (final Exception e) {
                            return Promises.newExceptionPromise(asResourceException(e));
                        }
                    }
                }).thenFinally(close(requestState));
                });
    }
    @Override
    public Promise<ResourceResponse, ResourceException> patchInstance(
            final Context context, final String resourceId, final PatchRequest request) {
        final RequestState requestState = wrap(context);
        final Connection connection = context.asContext(AuthenticatedConnectionContext.class).getConnection();
        if (request.getPatchOperations().isEmpty()) {
            return emptyPatchInstance(requestState, resourceId, request);
            return emptyPatchInstance(connection, resourceId, request);
        }
        final AtomicReference<Connection> connectionHolder = new AtomicReference<>();
        return requestState.getConnection()
                .thenOnResult(saveConnection(connectionHolder))
                .thenAsync(doUpdateFunction(requestState, resourceId, request.getRevision()))
        return doUpdateFunction(connection, resourceId, request.getRevision())
                .thenAsync(new AsyncFunction<DN, ResourceResponse, ResourceException>() {
                    @Override
                    public Promise<ResourceResponse, ResourceException> apply(final DN dn) throws ResourceException {
@@ -316,7 +290,7 @@
                        List<Promise<List<Modification>, ResourceException>> promises =
                                new ArrayList<>(request.getPatchOperations().size());
                        for (final PatchOperation operation : request.getPatchOperations()) {
                            promises.add(attributeMapper.patch(requestState, new JsonPointer(), operation));
                            promises.add(attributeMapper.patch(connection, new JsonPointer(), operation));
                        }
                        return Promises.when(promises).thenAsync(
@@ -336,13 +310,14 @@
                                            }
                                            final List<String> attributes =
                                                    asList(getLDAPAttributes(requestState, request.getFields()));
                                                    asList(getLDAPAttributes(connection, request.getFields()));
                                            if (modifyRequest.getModifications().isEmpty()) {
                                                // This patch is a no-op so just read the entry and check its version.
                                                return connectionHolder.get()
                                                        .readEntryAsync(dn, attributes)
                                                        .thenAsync(postEmptyPatchAsyncFunction(requestState, request),
                                                                   ldapExceptionToResourceException());
                                                return
                                                   connection
                                                     .readEntryAsync(dn, attributes)
                                                     .thenAsync(postEmptyPatchAsyncFunction(connection, request),
                                                                Exceptions.<ResourceResponse>toResourceException());
                                            } else {
                                                // Add controls and perform the modify request.
                                                if (config.readOnUpdatePolicy() == CONTROLS) {
@@ -354,10 +329,11 @@
                                                            PermissiveModifyRequestControl.newControl(true));
                                                }
                                                addAssertionControl(modifyRequest, request.getRevision());
                                                return connectionHolder.get()
                                                return connection
                                                        .applyChangeAsync(modifyRequest)
                                                        .thenAsync(postUpdateResultAsyncFunction(requestState),
                                                                   ldapExceptionToResourceException());
                                                        .thenAsync(
                                                                postUpdateResultAsyncFunction(connection),
                                                                Exceptions.<ResourceResponse>toResourceException());
                                            }
                                        } catch (final Exception e) {
                                            return Promises.newExceptionPromise(asResourceException(e));
@@ -365,27 +341,21 @@
                                    }
                                });
                    }
                }).thenFinally(close(requestState));
    }
    /** Just read the entry and check its version. */
    private Promise<ResourceResponse, ResourceException> emptyPatchInstance(
            final RequestState requestState, final String resourceId, final PatchRequest request) {
        return requestState.getConnection()
                .thenAsync(new AsyncFunction<Connection, ResourceResponse, ResourceException>() {
                    @Override
                    public Promise<ResourceResponse, ResourceException> apply(final Connection connection)
                            throws ResourceException {
                        SearchRequest searchRequest = searchRequest(requestState, resourceId, request.getFields());
                        return connection.searchSingleEntryAsync(searchRequest)
                                         .thenAsync(postEmptyPatchAsyncFunction(requestState, request),
                                                    ldapExceptionToResourceException());
                    }
                });
    }
    /** Just read the entry and check its version. */
    private Promise<ResourceResponse, ResourceException> emptyPatchInstance(final Connection connection,
            final String resourceId, final PatchRequest request) {
        final SearchRequest searchRequest = searchRequest(connection, resourceId, request.getFields());
        return connection
                .searchSingleEntryAsync(searchRequest)
                .thenAsync(postEmptyPatchAsyncFunction(connection, request),
                           Exceptions.<ResourceResponse>toResourceException());
    }
    private AsyncFunction<SearchResultEntry, ResourceResponse, ResourceException> postEmptyPatchAsyncFunction(
            final RequestState requestState, final PatchRequest request) {
            final Connection connection, final PatchRequest request) {
        return new AsyncFunction<SearchResultEntry, ResourceResponse, ResourceException>() {
            @Override
            public Promise<ResourceResponse, ResourceException> apply(SearchResultEntry entry)
@@ -393,7 +363,7 @@
                try {
                    // Fail if there is a version mismatch.
                    ensureMVCCVersionMatches(entry, request.getRevision());
                    return adaptEntry(requestState, entry);
                    return adaptEntry(connection, entry);
                } catch (final Exception e) {
                    return Promises.newExceptionPromise(asResourceException(e));
                }
@@ -404,23 +374,14 @@
    @Override
    public Promise<QueryResponse, ResourceException> queryCollection(
            final Context context, final QueryRequest request, final QueryResourceHandler resourceHandler) {
        final RequestState requestState = wrap(context);
        return requestState.getConnection()
                .thenAsync(new AsyncFunction<Connection, QueryResponse, ResourceException>() {
                    @Override
                    public Promise<QueryResponse, ResourceException> apply(final Connection connection)
                            throws ResourceException {
                        // Calculate the filter (this may require the connection).
                        return getLDAPFilter(requestState, request.getQueryFilter())
                                            .thenAsync(runQuery(request, resourceHandler, requestState, connection));
                    }
                })
                .thenFinally(close(requestState));
        final Connection connection = context.asContext(AuthenticatedConnectionContext.class).getConnection();
        // Calculate the filter (this may require the connection).
        return getLDAPFilter(connection, request.getQueryFilter())
                .thenAsync(runQuery(request, resourceHandler, connection));
    }
    private Promise<Filter, ResourceException> getLDAPFilter(
            final RequestState requestState, final QueryFilter<JsonPointer> queryFilter) {
    private Promise<Filter, ResourceException> getLDAPFilter(final Connection connection,
            final QueryFilter<JsonPointer> queryFilter) {
        final QueryFilterVisitor<Promise<Filter, ResourceException>, Void, JsonPointer> visitor =
                new QueryFilterVisitor<Promise<Filter, ResourceException>, Void, JsonPointer>() {
@@ -467,34 +428,34 @@
                    public Promise<Filter, ResourceException> visitContainsFilter(
                            final Void unused, final JsonPointer field, final Object valueAssertion) {
                        return attributeMapper.getLDAPFilter(
                                requestState, new JsonPointer(), field, FilterType.CONTAINS, null, valueAssertion);
                                connection, new JsonPointer(), field, FilterType.CONTAINS, null, valueAssertion);
                    }
                    @Override
                    public Promise<Filter, ResourceException> visitEqualsFilter(
                            final Void unused, final JsonPointer field, final Object valueAssertion) {
                        return attributeMapper.getLDAPFilter(
                                requestState, new JsonPointer(), field, FilterType.EQUAL_TO, null, valueAssertion);
                                connection, new JsonPointer(), field, FilterType.EQUAL_TO, null, valueAssertion);
                    }
                    @Override
                    public Promise<Filter, ResourceException> visitExtendedMatchFilter(final Void unused,
                            final JsonPointer field, final String operator, final Object valueAssertion) {
                        return attributeMapper.getLDAPFilter(
                                requestState, new JsonPointer(), field, FilterType.EXTENDED, operator, valueAssertion);
                                connection, new JsonPointer(), field, FilterType.EXTENDED, operator, valueAssertion);
                    }
                    @Override
                    public Promise<Filter, ResourceException> visitGreaterThanFilter(
                            final Void unused, final JsonPointer field, final Object valueAssertion) {
                        return attributeMapper.getLDAPFilter(
                                requestState, new JsonPointer(), field, FilterType.GREATER_THAN, null, valueAssertion);
                                connection, new JsonPointer(), field, FilterType.GREATER_THAN, null, valueAssertion);
                    }
                    @Override
                    public Promise<Filter, ResourceException> visitGreaterThanOrEqualToFilter(
                            final Void unused, final JsonPointer field, final Object valueAssertion) {
                        return attributeMapper.getLDAPFilter(requestState, new JsonPointer(), field,
                        return attributeMapper.getLDAPFilter(connection, new JsonPointer(), field,
                                FilterType.GREATER_THAN_OR_EQUAL_TO, null, valueAssertion);
                    }
@@ -502,13 +463,13 @@
                    public Promise<Filter, ResourceException> visitLessThanFilter(
                            final Void unused, final JsonPointer field, final Object valueAssertion) {
                        return attributeMapper.getLDAPFilter(
                                requestState, new JsonPointer(), field, FilterType.LESS_THAN, null, valueAssertion);
                                connection, new JsonPointer(), field, FilterType.LESS_THAN, null, valueAssertion);
                    }
                    @Override
                    public Promise<Filter, ResourceException> visitLessThanOrEqualToFilter(
                            final Void unused, final JsonPointer field, final Object valueAssertion) {
                        return attributeMapper.getLDAPFilter(requestState, new JsonPointer(), field,
                        return attributeMapper.getLDAPFilter(connection, new JsonPointer(), field,
                                FilterType.LESS_THAN_OR_EQUAL_TO, null, valueAssertion);
                    }
@@ -566,14 +527,14 @@
                    public Promise<Filter, ResourceException> visitPresentFilter(
                            final Void unused, final JsonPointer field) {
                        return attributeMapper.getLDAPFilter(
                                requestState, new JsonPointer(), field, FilterType.PRESENT, null, null);
                                connection, new JsonPointer(), field, FilterType.PRESENT, null, null);
                    }
                    @Override
                    public Promise<Filter, ResourceException> visitStartsWithFilter(
                            final Void unused, final JsonPointer field, final Object valueAssertion) {
                        return attributeMapper.getLDAPFilter(
                                requestState, new JsonPointer(), field, FilterType.STARTS_WITH, null, valueAssertion);
                                connection, new JsonPointer(), field, FilterType.STARTS_WITH, null, valueAssertion);
                    }
                };
@@ -582,7 +543,7 @@
    }
    private AsyncFunction<Filter, QueryResponse, ResourceException> runQuery(final QueryRequest request,
            final QueryResourceHandler resourceHandler, final RequestState requestState, final Connection connection) {
            final QueryResourceHandler resourceHandler, final Connection connection) {
        return new AsyncFunction<Filter, QueryResponse, ResourceException>() {
            /**
             * The following fields are guarded by sequenceLock. In addition,
@@ -604,7 +565,7 @@
                }
                final PromiseImpl<QueryResponse, ResourceException> promise = PromiseImpl.create();
                // Perform the search.
                final String[] attributes = getLDAPAttributes(requestState, request.getFields());
                final String[] attributes = getLDAPAttributes(connection, request.getFields());
                final Filter searchFilter = ldapFilter == Filter.alwaysTrue() ? Filter.objectClassPresent()
                                                                              : ldapFilter;
                final SearchRequest searchRequest = newSearchRequest(
@@ -660,9 +621,9 @@
                         * The best solution is probably to process the primary search results in batches using
                         * the paged results control.
                         */
                        final String id = nameStrategy.getResourceId(requestState, entry);
                        final String id = nameStrategy.getResourceId(connection, entry);
                        final String revision = getRevisionFromEntry(entry);
                        attributeMapper.read(requestState, new JsonPointer(), entry)
                        attributeMapper.read(connection, new JsonPointer(), entry)
                                       .thenOnResult(new ResultHandler<JsonValue>() {
                                           @Override
                                           public void handleResult(final JsonValue result) {
@@ -754,101 +715,84 @@
    @Override
    public Promise<ResourceResponse, ResourceException> readInstance(
            final Context context, final String resourceId, final ReadRequest request) {
        final RequestState requestState = wrap(context);
        return requestState.getConnection()
                .thenAsync(new AsyncFunction<Connection, ResourceResponse, ResourceException>() {
        final Connection connection = context.asContext(AuthenticatedConnectionContext.class).getConnection();
        // Do the search.
        SearchRequest searchRequest = searchRequest(connection, resourceId, request.getFields());
        return connection
                .searchSingleEntryAsync(searchRequest)
                .thenAsync(new AsyncFunction<SearchResultEntry, ResourceResponse, ResourceException>() {
                    @Override
                    public Promise<ResourceResponse, ResourceException> apply(Connection connection)
                    public Promise<ResourceResponse, ResourceException> apply(SearchResultEntry entry)
                            throws ResourceException {
                        // Do the search.
                        SearchRequest searchRequest = searchRequest(requestState, resourceId, request.getFields());
                        return connection.searchSingleEntryAsync(searchRequest)
                                    .thenAsync(
                                        new AsyncFunction<SearchResultEntry, ResourceResponse, ResourceException>() {
                                            @Override
                                            public Promise<ResourceResponse, ResourceException> apply(
                                                    SearchResultEntry entry) throws ResourceException {
                                                return adaptEntry(requestState, entry);
                                            }
                                        },
                                        ldapExceptionToResourceException());
                        return adaptEntry(connection, entry);
                    }
                })
                .thenFinally(close(requestState));
                }, Exceptions.<ResourceResponse>toResourceException());
    }
    @Override
    public Promise<ResourceResponse, ResourceException> updateInstance(
            final Context context, final String resourceId, final UpdateRequest request) {
        final RequestState requestState = wrap(context);
        final AtomicReference<Connection> connectionHolder = new AtomicReference<>();
        return requestState.getConnection().thenOnResult(saveConnection(connectionHolder))
                .thenAsync(new AsyncFunction<Connection, ResourceResponse, ResourceException>() {
        final Connection connection = context.asContext(AuthenticatedConnectionContext.class).getConnection();
        List<JsonPointer> attrs = Collections.emptyList();
        SearchRequest searchRequest = searchRequest(connection, resourceId, attrs);
        return connection
                .searchSingleEntryAsync(searchRequest)
                .thenAsync(new AsyncFunction<SearchResultEntry, ResourceResponse, ResourceException>() {
                    @Override
                    public Promise<ResourceResponse, ResourceException> apply(final Connection connection)
                            throws ResourceException {
                        List<JsonPointer> attrs = Collections.emptyList();
                        SearchRequest searchRequest = searchRequest(requestState, resourceId, attrs);
                        return connection.searchSingleEntryAsync(searchRequest)
                                .thenAsync(new AsyncFunction<SearchResultEntry, ResourceResponse, ResourceException>() {
                                    @Override
                                    public Promise<ResourceResponse, ResourceException> apply(
                                            final SearchResultEntry entry) {
                                        try {
                                            // Fail-fast if there is a version mismatch.
                                            ensureMVCCVersionMatches(entry, request.getRevision());
                    public Promise<ResourceResponse, ResourceException> apply(
                            final SearchResultEntry entry) {
                        try {
                            // Fail-fast if there is a version mismatch.
                            ensureMVCCVersionMatches(entry, request.getRevision());
                                            // Create the modify request.
                                            final ModifyRequest modifyRequest = newModifyRequest(entry.getName());
                                            if (config.readOnUpdatePolicy() == CONTROLS) {
                                                final String[] attributes =
                                                        getLDAPAttributes(requestState, request.getFields());
                                                modifyRequest.addControl(
                                                        PostReadRequestControl.newControl(false, attributes));
                                            }
                                            if (config.usePermissiveModify()) {
                                                modifyRequest.addControl(
                                                        PermissiveModifyRequestControl.newControl(true));
                                            }
                                            addAssertionControl(modifyRequest, request.getRevision());
                            // Create the modify request.
                            final ModifyRequest modifyRequest = newModifyRequest(entry.getName());
                            if (config.readOnUpdatePolicy() == CONTROLS) {
                                final String[] attributes =
                                        getLDAPAttributes(connection, request.getFields());
                                modifyRequest.addControl(
                                        PostReadRequestControl.newControl(false, attributes));
                            }
                            if (config.usePermissiveModify()) {
                                modifyRequest.addControl(
                                        PermissiveModifyRequestControl.newControl(true));
                            }
                            addAssertionControl(modifyRequest, request.getRevision());
                                            // Determine the set of changes that need to be performed.
                                            return attributeMapper.update(
                                                        requestState, new JsonPointer(), entry, request.getContent())
                                                    .thenAsync(new AsyncFunction<
                                                            List<Modification>, ResourceResponse, ResourceException>() {
                                                        @Override
                                                        public Promise<ResourceResponse, ResourceException> apply(
                                                                List<Modification> modifications)
                                                                throws ResourceException {
                                                            if (modifications.isEmpty()) {
                                                                // No changes to be performed so just return
                                                                // the entry that we read.
                                                                return adaptEntry(requestState, entry);
                                                            }
                                                            // Perform the modify operation.
                                                            modifyRequest.getModifications().addAll(modifications);
                                                            return connection.applyChangeAsync(modifyRequest).thenAsync(
                                                                    postUpdateResultAsyncFunction(requestState),
                                                                    ldapExceptionToResourceException());
                                                        }
                                                    });
                                        } catch (final Exception e) {
                                            return Promises.newExceptionPromise(asResourceException(e));
                            // Determine the set of changes that need to be performed.
                            return attributeMapper.update(
                                    connection, new JsonPointer(), entry, request.getContent())
                                    .thenAsync(new AsyncFunction<
                                            List<Modification>, ResourceResponse, ResourceException>() {
                                        @Override
                                        public Promise<ResourceResponse, ResourceException> apply(
                                                List<Modification> modifications)
                                                throws ResourceException {
                                            if (modifications.isEmpty()) {
                                                // No changes to be performed so just return
                                                // the entry that we read.
                                                return adaptEntry(connection, entry);
                                            }
                                            // Perform the modify operation.
                                            modifyRequest.getModifications().addAll(modifications);
                                            return connection
                                                    .applyChangeAsync(modifyRequest)
                                                    .thenAsync(
                                                            postUpdateResultAsyncFunction(connection),
                                                            Exceptions.<ResourceResponse>toResourceException());
                                        }
                                    }
                                }, ldapExceptionToResourceException());
                                    });
                        } catch (final Exception e) {
                            return Promises.newExceptionPromise(asResourceException(e));
                        }
                    }
                }).thenFinally(close(requestState));
                }, Exceptions.<ResourceResponse>toResourceException());
    }
    private Promise<ResourceResponse, ResourceException> adaptEntry(
            final RequestState requestState, final Entry entry) {
        final String actualResourceId = nameStrategy.getResourceId(requestState, entry);
    private Promise<ResourceResponse, ResourceException> adaptEntry(final Connection connection, final Entry entry) {
        final String actualResourceId = nameStrategy.getResourceId(connection, entry);
        final String revision = getRevisionFromEntry(entry);
        return attributeMapper.read(requestState, new JsonPointer(), entry)
        return attributeMapper.read(connection, new JsonPointer(), entry)
                              .then(new Function<JsonValue, ResourceResponse, ResourceException>() {
                                  @Override
                                  public ResourceResponse apply(final JsonValue value) {
@@ -867,43 +811,35 @@
        }
    }
    private AsyncFunction<Connection, DN, ResourceException> doUpdateFunction(
            final RequestState requestState, final String resourceId, final String revision) {
        return new AsyncFunction<Connection, DN, ResourceException>() {
            @Override
            public Promise<DN, ResourceException> apply(Connection connection) {
                final String ldapAttribute =
                        (etagAttribute != null && revision != null) ? etagAttribute.toString() : "1.1";
                final SearchRequest searchRequest =
                        nameStrategy.createSearchRequest(requestState, getBaseDN(), resourceId)
                                    .addAttribute(ldapAttribute);
                if (searchRequest.getScope().equals(SearchScope.BASE_OBJECT)) {
                    // There's no point in doing a search because we already know the DN.
                    return Promises.newResultPromise(searchRequest.getName());
                }
                return connection.searchSingleEntryAsync(searchRequest)
                        .thenAsync(new AsyncFunction<SearchResultEntry, DN, ResourceException>() {
                            @Override
                            public Promise<DN, ResourceException> apply(SearchResultEntry entry)
                                    throws ResourceException {
                                try {
                                    // Fail-fast if there is a version mismatch.
                                    ensureMVCCVersionMatches(entry, revision);
                                    // Perform update operation.
                                    return Promises.newResultPromise(entry.getName());
                                } catch (final Exception e) {
                                    return Promises.newExceptionPromise(asResourceException(e));
                                }
                            }
                        }, new AsyncFunction<LdapException, DN, ResourceException>() {
                            @Override
                            public Promise<DN, ResourceException> apply(LdapException ldapException)
                                    throws ResourceException {
                                return Promises.newExceptionPromise(asResourceException(ldapException));
                            }
                        });
            }
        };
    private Promise<DN, ResourceException> doUpdateFunction(final Connection connection, final String resourceId,
            final String revision) {
        final String ldapAttribute = (etagAttribute != null && revision != null) ? etagAttribute.toString() : "1.1";
        final SearchRequest searchRequest = nameStrategy.createSearchRequest(connection, getBaseDN(), resourceId)
                .addAttribute(ldapAttribute);
        if (searchRequest.getScope().equals(SearchScope.BASE_OBJECT)) {
            // There's no point in doing a search because we already know the DN.
            return Promises.newResultPromise(searchRequest.getName());
        }
        return connection
                .searchSingleEntryAsync(searchRequest)
                .thenAsync(new AsyncFunction<SearchResultEntry, DN, ResourceException>() {
                    @Override
                    public Promise<DN, ResourceException> apply(SearchResultEntry entry) throws ResourceException {
                        try {
                            // Fail-fast if there is a version mismatch.
                            ensureMVCCVersionMatches(entry, revision);
                            // Perform update operation.
                            return Promises.newResultPromise(entry.getName());
                        } catch (final Exception e) {
                            return Promises.newExceptionPromise(asResourceException(e));
                        }
                    }
                }, new AsyncFunction<LdapException, DN, ResourceException>() {
                    @Override
                    public Promise<DN, ResourceException> apply(LdapException ldapException) throws ResourceException {
                        return Promises.newExceptionPromise(asResourceException(ldapException));
                    }
                });
    }
    private void ensureMVCCSupported() throws NotSupportedException {
@@ -937,33 +873,32 @@
     * Determines the set of LDAP attributes to request in an LDAP read (search,
     * post-read), based on the provided list of JSON pointers.
     *
     * @param requestState
     * @param connection
     *          The request state.
     * @param requestedAttributes
     *          The list of resource attributes to be read.
     * @return The set of LDAP attributes associated with the resource
     *         attributes.
     */
    private String[] getLDAPAttributes(
            final RequestState requestState, final Collection<JsonPointer> requestedAttributes) {
    private String[] getLDAPAttributes(final Connection connection, final Collection<JsonPointer> requestedAttributes) {
        // Get all the LDAP attributes required by the attribute mappers.
        final Set<String> requestedLDAPAttributes;
        if (requestedAttributes.isEmpty()) {
            // Full read.
            requestedLDAPAttributes = new LinkedHashSet<>();
            attributeMapper.getLDAPAttributes(requestState, new JsonPointer(), new JsonPointer(),
            attributeMapper.getLDAPAttributes(connection, new JsonPointer(), new JsonPointer(),
                    requestedLDAPAttributes);
        } else {
            // Partial read.
            requestedLDAPAttributes = new LinkedHashSet<>(requestedAttributes.size());
            for (final JsonPointer requestedAttribute : requestedAttributes) {
                attributeMapper.getLDAPAttributes(requestState, new JsonPointer(), requestedAttribute,
                attributeMapper.getLDAPAttributes(connection, new JsonPointer(), requestedAttribute,
                        requestedLDAPAttributes);
            }
        }
        // Get the LDAP attributes required by the Etag and name stategies.
        nameStrategy.getLDAPAttributes(requestState, requestedLDAPAttributes);
        nameStrategy.getLDAPAttributes(connection, requestedLDAPAttributes);
        if (etagAttribute != null) {
            requestedLDAPAttributes.add(etagAttribute.toString());
        }
@@ -975,7 +910,7 @@
    }
    private AsyncFunction<Result, ResourceResponse, ResourceException> postUpdateResultAsyncFunction(
            final RequestState requestState) {
            final Connection connection) {
        // The handler which will be invoked for the LDAP add result.
        return new AsyncFunction<Result, ResourceResponse, ResourceException>() {
            @Override
@@ -1001,7 +936,7 @@
                    entry = null;
                }
                if (entry != null) {
                    return adaptEntry(requestState, entry);
                    return adaptEntry(connection, entry);
                } else {
                    return Promises.newResultPromise(
                            Responses.newResourceResponse(null, null, new JsonValue(Collections.emptyMap())));
@@ -1010,45 +945,21 @@
        };
    }
    private AsyncFunction<LdapException, ResourceResponse, ResourceException> ldapExceptionToResourceException() {
        return ldapToResourceException();
    }
    private <R> AsyncFunction<LdapException, R, ResourceException> ldapToResourceException() {
        // The handler which will be invoked for the LDAP add result.
        return new AsyncFunction<LdapException, R, ResourceException>() {
            @Override
            public Promise<R, ResourceException> apply(final LdapException ldapException) throws ResourceException {
                return Promises.newExceptionPromise(asResourceException(ldapException));
            }
        };
    }
    private SearchRequest searchRequest(
            final RequestState requestState, final String resourceId, final List<JsonPointer> requestedAttributes) {
        final String[] attributes = getLDAPAttributes(requestState, requestedAttributes);
        return nameStrategy.createSearchRequest(requestState, getBaseDN(), resourceId).addAttribute(attributes);
            final Connection connection, final String resourceId, final List<JsonPointer> requestedAttributes) {
        final String[] attributes = getLDAPAttributes(connection, requestedAttributes);
        return nameStrategy.createSearchRequest(connection, getBaseDN(), resourceId).addAttribute(attributes);
    }
    private RequestState wrap(final Context context) {
        return new RequestState(config, context);
    }
    private Runnable close(final RequestState requestState) {
        return new Runnable() {
            @Override
            public void run() {
                requestState.close();
            }
        };
    }
    private ResultHandler<Connection> saveConnection(final AtomicReference<Connection> connectionHolder) {
        return new ResultHandler<Connection>() {
            @Override
            public void handleResult(Connection connection) {
                connectionHolder.set(connection);
            }
        };
    private static final class Exceptions {
        private static <R> AsyncFunction<LdapException, R, ResourceException> toResourceException() {
            // The handler which will be invoked for the LDAP add result.
            return new AsyncFunction<LdapException, R, ResourceException>() {
                @Override
                public Promise<R, ResourceException> apply(final LdapException ldapException) throws ResourceException {
                    return Promises.newExceptionPromise(asResourceException(ldapException));
                }
            };
        }
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/NameStrategy.java
@@ -11,7 +11,7 @@
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2013-2015 ForgeRock AS.
 * Copyright 2013-2016 ForgeRock AS.
 */
package org.forgerock.opendj.rest2ldap;
@@ -19,6 +19,7 @@
import java.util.Set;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.opendj.ldap.Connection;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.requests.SearchRequest;
@@ -40,8 +41,8 @@
     * Returns a search request which can be used to obtain the specified REST
     * resource.
     *
     * @param requestState
     *            The request state.
     * @param connection
     *            The LDAP connection to use to perform the operation.
     * @param baseDN
     *            The search base DN.
     * @param resourceId
@@ -49,40 +50,40 @@
     * @return A search request which can be used to obtain the specified REST
     *         resource.
     */
    abstract SearchRequest createSearchRequest(RequestState requestState, DN baseDN, String resourceId);
    abstract SearchRequest createSearchRequest(Connection connection, DN baseDN, String resourceId);
    /**
     * Adds the name of any LDAP attribute required by this name strategy to the
     * provided set.
     *
     * @param requestState
     *            The request state.
     * @param connection
     *            The LDAP connection to use to perform the operation.
     * @param ldapAttributes
     *            The set into which any required LDAP attribute name should be
     *            put.
     */
    abstract void getLDAPAttributes(RequestState requestState, Set<String> ldapAttributes);
    abstract void getLDAPAttributes(Connection connection, Set<String> ldapAttributes);
    /**
     * Retrieves the resource ID from the provided LDAP entry. Implementations
     * may use the entry DN as well as any attributes in order to determine the
     * resource ID.
     *
     * @param requestState
     *            The request state.
     * @param connection
     *            The LDAP connection to use to perform the operation.
     * @param entry
     *            The LDAP entry from which the resource ID should be obtained.
     * @return The resource ID.
     */
    abstract String getResourceId(RequestState requestState, Entry entry);
    abstract String getResourceId(Connection connection, Entry entry);
    /**
     * Sets the resource ID in the provided LDAP entry. Implementations are
     * responsible for setting the entry DN as well as any attributes associated
     * with the resource ID.
     *
     * @param requestState
     *            The request state.
     * @param connection
     *            The LDAP connection to use to perform the operation.
     * @param baseDN
     *            The baseDN to use when constructing the entry's DN.
     * @param resourceId
@@ -93,7 +94,7 @@
     * @throws ResourceException
     *             If the resource ID cannot be determined.
     */
    abstract void setResourceId(RequestState requestState, DN baseDN, String resourceId, Entry entry)
    abstract void setResourceId(Connection connection, DN baseDN, String resourceId, Entry entry)
            throws ResourceException;
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java
@@ -11,7 +11,7 @@
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2012-2015 ForgeRock AS.
 * Copyright 2012-2016 ForgeRock AS.
 */
package org.forgerock.opendj.rest2ldap;
@@ -35,6 +35,7 @@
import org.forgerock.json.resource.PatchOperation;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.opendj.ldap.Attribute;
import org.forgerock.opendj.ldap.Connection;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.Modification;
@@ -87,8 +88,8 @@
    }
    @Override
    Promise<List<Attribute>, ResourceException> create(
            final RequestState requestState, final JsonPointer path, final JsonValue v) {
    Promise<List<Attribute>, ResourceException> create(final Connection connection, final JsonPointer path,
            final JsonValue v) {
        try {
            /*
             * First check that the JSON value is an object and that the fields
@@ -104,13 +105,13 @@
                for (final Map.Entry<String, Object> me : v.asMap().entrySet()) {
                    final Mapping mapping = getMapping(me.getKey());
                    final JsonValue subValue = new JsonValue(me.getValue());
                    promises.add(mapping.mapper.create(requestState, path.child(me.getKey()), subValue));
                    promises.add(mapping.mapper.create(connection, path.child(me.getKey()), subValue));
                }
            }
            // Invoke mappings for which there were no values provided.
            for (final Mapping mapping : missingMappings.values()) {
                promises.add(mapping.mapper.create(requestState, path.child(mapping.name), null));
                promises.add(mapping.mapper.create(connection, path.child(mapping.name), null));
            }
            return Promises.when(promises)
@@ -121,29 +122,29 @@
    }
    @Override
    void getLDAPAttributes(final RequestState requestState, final JsonPointer path, final JsonPointer subPath,
    void getLDAPAttributes(final Connection connection, final JsonPointer path, final JsonPointer subPath,
            final Set<String> ldapAttributes) {
        if (subPath.isEmpty()) {
            // Request all subordinate mappings.
            for (final Mapping mapping : mappings.values()) {
                mapping.mapper.getLDAPAttributes(requestState, path.child(mapping.name), subPath, ldapAttributes);
                mapping.mapper.getLDAPAttributes(connection, path.child(mapping.name), subPath, ldapAttributes);
            }
        } else {
            // Request single subordinate mapping.
            final Mapping mapping = getMapping(subPath);
            if (mapping != null) {
                mapping.mapper.getLDAPAttributes(
                        requestState, path.child(subPath.get(0)), subPath.relativePointer(), ldapAttributes);
                        connection, path.child(subPath.get(0)), subPath.relativePointer(), ldapAttributes);
            }
        }
    }
    @Override
    Promise<Filter, ResourceException> getLDAPFilter(final RequestState requestState, final JsonPointer path,
    Promise<Filter, ResourceException> getLDAPFilter(final Connection connection, final JsonPointer path,
            final JsonPointer subPath, final FilterType type, final String operator, final Object valueAssertion) {
        final Mapping mapping = getMapping(subPath);
        if (mapping != null) {
            return mapping.mapper.getLDAPFilter(requestState, path.child(subPath.get(0)),
            return mapping.mapper.getLDAPFilter(connection, path.child(subPath.get(0)),
                    subPath.relativePointer(), type, operator, valueAssertion);
        } else {
            /*
@@ -156,8 +157,8 @@
    }
    @Override
    Promise<List<Modification>, ResourceException> patch(
            final RequestState requestState, final JsonPointer path, final PatchOperation operation) {
    Promise<List<Modification>, ResourceException> patch(final Connection connection, final JsonPointer path,
            final PatchOperation operation) {
        try {
            final JsonPointer field = operation.getField();
            final JsonValue v = operation.getValue();
@@ -180,7 +181,7 @@
                        final JsonValue subValue = new JsonValue(me.getValue());
                        final PatchOperation subOperation =
                                operation(operation.getOperation(), field /* empty */, subValue);
                        promises.add(mapping.mapper.patch(requestState, path.child(me.getKey()), subOperation));
                        promises.add(mapping.mapper.patch(connection, path.child(me.getKey()), subOperation));
                    }
                }
@@ -201,7 +202,7 @@
                }
                final PatchOperation subOperation =
                        operation(operation.getOperation(), field.relativePointer(), v);
                return mapping.mapper.patch(requestState, path.child(fieldName), subOperation);
                return mapping.mapper.patch(connection, path.child(fieldName), subOperation);
            }
        } catch (final Exception ex) {
            return Promises.newExceptionPromise(asResourceException(ex));
@@ -209,7 +210,7 @@
    }
    @Override
    Promise<JsonValue, ResourceException> read(final RequestState requestState, final JsonPointer path, final Entry e) {
    Promise<JsonValue, ResourceException> read(final Connection connection, final JsonPointer path, final Entry e) {
        /*
         * Use an accumulator which will aggregate the results from the
         * subordinate mappers into a single list. On completion, the
@@ -219,7 +220,7 @@
                new ArrayList<>(mappings.size());
        for (final Mapping mapping : mappings.values()) {
            promises.add(mapping.mapper.read(requestState, path.child(mapping.name), e)
            promises.add(mapping.mapper.read(connection, path.child(mapping.name), e)
                    .then(new Function<JsonValue, Map.Entry<String, JsonValue>, ResourceException>() {
                        @Override
                        public Map.Entry<String, JsonValue> apply(final JsonValue value) {
@@ -255,7 +256,7 @@
    @Override
    Promise<List<Modification>, ResourceException> update(
            final RequestState requestState, final JsonPointer path, final Entry e, final JsonValue v) {
            final Connection connection, final JsonPointer path, final Entry e, final JsonValue v) {
        try {
            // First check that the JSON value is an object and that the fields
            // it contains are known by this mapper.
@@ -269,13 +270,13 @@
                for (final Map.Entry<String, Object> me : v.asMap().entrySet()) {
                    final Mapping mapping = getMapping(me.getKey());
                    final JsonValue subValue = new JsonValue(me.getValue());
                    promises.add(mapping.mapper.update(requestState, path.child(me.getKey()), e, subValue));
                    promises.add(mapping.mapper.update(connection, path.child(me.getKey()), e, subValue));
                }
            }
            // Invoke mappings for which there were no values provided.
            for (final Mapping mapping : missingMappings.values()) {
                promises.add(mapping.mapper.update(requestState, path.child(mapping.name), e, null));
                promises.add(mapping.mapper.update(connection, path.child(mapping.name), e, null));
            }
            return Promises.when(promises)
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java
@@ -11,7 +11,7 @@
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2012-2015 ForgeRock AS.
 * Copyright 2012-2016 ForgeRock AS.
 */
package org.forgerock.opendj.rest2ldap;
@@ -51,6 +51,7 @@
import org.forgerock.opendj.ldap.responses.Result;
import org.forgerock.opendj.ldap.responses.SearchResultEntry;
import org.forgerock.opendj.ldap.responses.SearchResultReference;
import org.forgerock.opendj.ldap.schema.Schema;
import org.forgerock.util.AsyncFunction;
import org.forgerock.util.Function;
import org.forgerock.util.promise.ExceptionHandler;
@@ -70,14 +71,16 @@
    private static final int SEARCH_MAX_CANDIDATES = 1000;
    private final DN baseDN;
    private final Schema schema;
    private Filter filter;
    private final AttributeMapper mapper;
    private final AttributeDescription primaryKey;
    private SearchScope scope = SearchScope.WHOLE_SUBTREE;
    ReferenceAttributeMapper(final AttributeDescription ldapAttributeName, final DN baseDN,
    ReferenceAttributeMapper(final Schema schema, final AttributeDescription ldapAttributeName, final DN baseDN,
        final AttributeDescription primaryKey, final AttributeMapper mapper) {
        super(ldapAttributeName);
        this.schema = schema;
        this.baseDN = baseDN;
        this.primaryKey = primaryKey;
        this.mapper = mapper;
@@ -130,10 +133,9 @@
    }
    @Override
    Promise<Filter, ResourceException> getLDAPFilter(final RequestState requestState, final JsonPointer path,
    Promise<Filter, ResourceException> getLDAPFilter(final Connection connection, final JsonPointer path,
            final JsonPointer subPath, final FilterType type, final String operator, final Object valueAssertion) {
        return mapper.getLDAPFilter(requestState, path, subPath, type, operator, valueAssertion)
        return mapper.getLDAPFilter(connection, path, subPath, type, operator, valueAssertion)
                .thenAsync(new AsyncFunction<Filter, Filter, ResourceException>() {
                    @Override
                    public Promise<Filter, ResourceException> apply(final Filter result) {
@@ -141,56 +143,47 @@
                        final SearchRequest request = createSearchRequest(result);
                        final List<Filter> subFilters = new LinkedList<>();
                        return requestState.getConnection().thenAsync(
                                new AsyncFunction<Connection, Filter, ResourceException>() {
                                    @Override
                                    public Promise<Filter, ResourceException> apply(final Connection connection)
                                            throws ResourceException {
                                        return connection.searchAsync(request, new SearchResultHandler() {
                                            @Override
                                            public boolean handleEntry(final SearchResultEntry entry) {
                                                if (subFilters.size() < SEARCH_MAX_CANDIDATES) {
                                                    subFilters.add(Filter.equality(
                                                            ldapAttributeName.toString(), entry.getName()));
                                                    return true;
                                                } else {
                                                    // No point in continuing - maximum candidates reached.
                                                    return false;
                                                }
                                            }
                                            @Override
                                            public boolean handleReference(final SearchResultReference reference) {
                                                // Ignore references.
                                                return true;
                                            }
                                        }).then(new Function<Result, Filter, ResourceException>() {
                                            @Override
                                            public Filter apply(Result result) throws ResourceException {
                                                if (subFilters.size() >= SEARCH_MAX_CANDIDATES) {
                                                    throw asResourceException(
                                                            newLdapException(ResultCode.ADMIN_LIMIT_EXCEEDED));
                                                } else if (subFilters.size() == 1) {
                                                    return subFilters.get(0);
                                                } else {
                                                    return Filter.or(subFilters);
                                                }
                                            }
                                        }, new Function<LdapException, Filter, ResourceException>() {
                                            @Override
                                            public Filter apply(LdapException exception) throws ResourceException {
                                                throw asResourceException(exception);
                                            }
                                        });
                                    }
                                });
                        return connection.searchAsync(request, new SearchResultHandler() {
                            @Override
                            public boolean handleEntry(final SearchResultEntry entry) {
                                if (subFilters.size() < SEARCH_MAX_CANDIDATES) {
                                    subFilters.add(Filter.equality(ldapAttributeName.toString(), entry.getName()));
                                    return true;
                                } else {
                                    // No point in continuing - maximum candidates reached.
                                    return false;
                                }
                            }
                            @Override
                            public boolean handleReference(final SearchResultReference reference) {
                                // Ignore references.
                                return true;
                            }
                        }).then(new Function<Result, Filter, ResourceException>() {
                            @Override
                            public Filter apply(Result result) throws ResourceException {
                                if (subFilters.size() >= SEARCH_MAX_CANDIDATES) {
                                    throw asResourceException(
                                            newLdapException(ResultCode.ADMIN_LIMIT_EXCEEDED));
                                } else if (subFilters.size() == 1) {
                                    return subFilters.get(0);
                                } else {
                                    return Filter.or(subFilters);
                                }
                            }
                        }, new Function<LdapException, Filter, ResourceException>() {
                            @Override
                            public Filter apply(LdapException exception) throws ResourceException {
                                throw asResourceException(exception);
                            }
                        });
                    }
                });
    }
    @Override
    Promise<Attribute, ResourceException> getNewLDAPAttributes(
            final RequestState requestState, final JsonPointer path, final List<Object> newValues) {
    Promise<Attribute, ResourceException> getNewLDAPAttributes(final Connection connection, final JsonPointer path,
            final List<Object> newValues) {
        /*
         * For each value use the subordinate mapper to obtain the LDAP primary
         * key, the perform a search for each one to find the corresponding entries.
@@ -201,7 +194,7 @@
        final PromiseImpl<Attribute, ResourceException> promise = PromiseImpl.create();
        for (final Object value : newValues) {
            mapper.create(requestState, path, new JsonValue(value)).thenOnResult(new ResultHandler<List<Attribute>>() {
            mapper.create(connection, path, new JsonValue(value)).thenOnResult(new ResultHandler<List<Attribute>>() {
                @Override
                public void handleResult(List<Attribute> result) {
                    Attribute primaryKeyAttribute = null;
@@ -228,43 +221,38 @@
                    final ByteString primaryKeyValue = primaryKeyAttribute.firstValue();
                    final Filter filter = Filter.equality(primaryKey.toString(), primaryKeyValue);
                    final SearchRequest search = createSearchRequest(filter);
                    requestState.getConnection().thenOnResult(new ResultHandler<Connection>() {
                        @Override
                        public void handleResult(Connection connection) {
                            connection.searchSingleEntryAsync(search)
                                      .thenOnResult(new ResultHandler<SearchResultEntry>() {
                                          @Override
                                          public void handleResult(final SearchResultEntry result) {
                                              synchronized (newLDAPAttribute) {
                                                  newLDAPAttribute.add(result.getName());
                                              }
                                              completeIfNecessary();
                                          }
                                      }).thenOnException(new ExceptionHandler<LdapException>() {
                                          @Override
                                          public void handleException(final LdapException error) {
                                              ResourceException re;
                                              try {
                                                  throw error;
                                              } catch (final EntryNotFoundException e) {
                                                  re = new BadRequestException(i18n(
                                                          "The request cannot be processed because the resource "
                                                          + "'%s' referenced in field '%s' does not exist",
                                                          primaryKeyValue.toString(), path));
                                              } catch (final MultipleEntriesFoundException e) {
                                                  re = new BadRequestException(i18n(
                                                          "The request cannot be processed because the resource "
                                                          + "'%s' referenced in field '%s' is ambiguous",
                                                          primaryKeyValue.toString(), path));
                                              } catch (final LdapException e) {
                                                  re = asResourceException(e);
                                              }
                                              exception.compareAndSet(null, re);
                                              completeIfNecessary();
                                          }
                                      });
                        }
                    });
                    connection.searchSingleEntryAsync(search)
                              .thenOnResult(new ResultHandler<SearchResultEntry>() {
                                  @Override
                                  public void handleResult(final SearchResultEntry result) {
                                      synchronized (newLDAPAttribute) {
                                          newLDAPAttribute.add(result.getName());
                                      }
                                      completeIfNecessary();
                                  }
                              }).thenOnException(new ExceptionHandler<LdapException>() {
                                  @Override
                                  public void handleException(final LdapException error) {
                                      ResourceException re;
                                      try {
                                          throw error;
                                      } catch (final EntryNotFoundException e) {
                                          re = new BadRequestException(i18n(
                                                  "The request cannot be processed because the resource "
                                                  + "'%s' referenced in field '%s' does not exist",
                                                  primaryKeyValue.toString(), path));
                                      } catch (final MultipleEntriesFoundException e) {
                                          re = new BadRequestException(i18n(
                                                  "The request cannot be processed because the resource "
                                                  + "'%s' referenced in field '%s' is ambiguous",
                                                  primaryKeyValue.toString(), path));
                                      } catch (final LdapException e) {
                                          re = asResourceException(e);
                                      }
                                      exception.compareAndSet(null, re);
                                      completeIfNecessary();
                                  }
                              });
                }
                private void completeIfNecessary() {
@@ -287,25 +275,25 @@
    }
    @Override
    Promise<JsonValue, ResourceException> read(final RequestState c, final JsonPointer path, final Entry e) {
    Promise<JsonValue, ResourceException> read(final Connection connection, final JsonPointer path, final Entry e) {
        final Attribute attribute = e.getAttribute(ldapAttributeName);
        if (attribute == null || attribute.isEmpty()) {
            return Promises.newResultPromise(null);
        } else if (attributeIsSingleValued()) {
            try {
                final DN dn = attribute.parse().usingSchema(c.getConfig().schema()).asDN();
                return readEntry(c, path, dn);
                final DN dn = attribute.parse().usingSchema(schema).asDN();
                return readEntry(connection, path, dn);
            } catch (final Exception ex) {
                // The LDAP attribute could not be decoded.
                return Promises.newExceptionPromise(asResourceException(ex));
            }
        } else {
            try {
                final Set<DN> dns = attribute.parse().usingSchema(c.getConfig().schema()).asSetOfDN();
                final Set<DN> dns = attribute.parse().usingSchema(schema).asSetOfDN();
                final List<Promise<JsonValue, ResourceException>> promises = new ArrayList<>(dns.size());
                for (final DN dn : dns) {
                    promises.add(readEntry(c, path, dn));
                    promises.add(readEntry(connection, path, dn));
                }
                return Promises.when(promises)
@@ -340,33 +328,30 @@
    }
    private Promise<JsonValue, ResourceException> readEntry(
            final RequestState requestState, final JsonPointer path, final DN dn) {
            final Connection connection, final JsonPointer path, final DN dn) {
        final Set<String> requestedLDAPAttributes = new LinkedHashSet<>();
        mapper.getLDAPAttributes(requestState, path, new JsonPointer(), requestedLDAPAttributes);
        return requestState.getConnection().thenAsync(new AsyncFunction<Connection, JsonValue, ResourceException>() {
            @Override
            public Promise<JsonValue, ResourceException> apply(Connection connection) throws ResourceException {
                final Filter searchFilter = filter != null ? filter : Filter.alwaysTrue();
                final String[] attributes = requestedLDAPAttributes.toArray(new String[requestedLDAPAttributes.size()]);
                final SearchRequest request = newSearchRequest(dn, SearchScope.BASE_OBJECT, searchFilter, attributes);
        mapper.getLDAPAttributes(connection, path, new JsonPointer(), requestedLDAPAttributes);
                return connection.searchSingleEntryAsync(request)
                        .thenAsync(new AsyncFunction<SearchResultEntry, JsonValue, ResourceException>() {
                            @Override
                            public Promise<JsonValue, ResourceException> apply(final SearchResultEntry result) {
                                return mapper.read(requestState, path, result);
                            }
                        }, new AsyncFunction<LdapException, JsonValue, ResourceException>() {
                            @Override
                            public Promise<JsonValue, ResourceException> apply(final LdapException error) {
                                if (error instanceof EntryNotFoundException) {
                                    // Ignore missing entry since it cannot be mapped.
                                    return Promises.newResultPromise(null);
                                }
                                return Promises.newExceptionPromise(asResourceException(error));
                            }
                        });
            }
        });
        final Filter searchFilter = filter != null ? filter : Filter.alwaysTrue();
        final String[] attributes = requestedLDAPAttributes.toArray(new String[requestedLDAPAttributes.size()]);
        final SearchRequest request = newSearchRequest(dn, SearchScope.BASE_OBJECT, searchFilter, attributes);
        return connection
                .searchSingleEntryAsync(request)
                .thenAsync(new AsyncFunction<SearchResultEntry, JsonValue, ResourceException>() {
                    @Override
                    public Promise<JsonValue, ResourceException> apply(final SearchResultEntry result) {
                        return mapper.read(connection, path, result);
                    }
                }, new AsyncFunction<LdapException, JsonValue, ResourceException>() {
                    @Override
                    public Promise<JsonValue, ResourceException> apply(final LdapException error) {
                        if (error instanceof EntryNotFoundException) {
                            // Ignore missing entry since it cannot be mapped.
                            return Promises.newResultPromise(null);
                        }
                        return Promises.newExceptionPromise(asResourceException(error));
                    }
                });
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/RequestState.java
File was deleted
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java
@@ -49,6 +49,7 @@
import org.forgerock.opendj.ldap.AuthenticationException;
import org.forgerock.opendj.ldap.AuthorizationException;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.Connection;
import org.forgerock.opendj.ldap.ConnectionException;
import org.forgerock.opendj.ldap.ConnectionFactory;
import org.forgerock.opendj.ldap.ConstraintViolationException;
@@ -92,12 +93,9 @@
    /** A builder for incrementally constructing LDAP resource collections. */
    public static final class Builder {
        private final List<Attribute> additionalLDAPAttributes = new LinkedList<>();
        private AuthorizationPolicy authzPolicy = AuthorizationPolicy.NONE;
        private DN baseDN; // TODO: support template variables.
        private AttributeDescription etagAttribute;
        private ConnectionFactory factory;
        private NameStrategy nameStrategy;
        private AuthzIdTemplate proxiedAuthzTemplate;
        private ReadOnUpdatePolicy readOnUpdatePolicy = CONTROLS;
        private AttributeMapper rootMapper;
        private Schema schema = Schema.getDefaultSchema();
@@ -141,18 +139,6 @@
        }
        /**
         * Sets the policy which should be for performing authorization.
         *
         * @param policy
         *            The policy which should be for performing authorization.
         * @return A reference to this LDAP resource collection builder.
         */
        public Builder authorizationPolicy(final AuthorizationPolicy policy) {
            this.authzPolicy = ensureNotNull(policy);
            return this;
        }
        /**
         * Sets the base DN beneath which LDAP entries (resources) are to be found.
         *
         * @param dn
@@ -186,30 +172,8 @@
            if (rootMapper == null) {
                throw new IllegalStateException("No mappings provided");
            }
            switch (authzPolicy) {
            case NONE:
                if (factory == null) {
                    throw new IllegalStateException(
                            "A connection factory must be specified when the authorization policy is 'none'");
                }
                break;
            case PROXY:
                if (proxiedAuthzTemplate == null) {
                    throw new IllegalStateException(
                            "Proxied authorization enabled but no template defined");
                }
                if (factory == null) {
                    throw new IllegalStateException(
                            "A connection factory must be specified when using proxied authorization");
                }
                break;
            case REUSE:
                // This is always ok.
                break;
            }
            return new LDAPCollectionResourceProvider(baseDN, rootMapper, nameStrategy,
                    etagAttribute, new Config(factory, readOnUpdatePolicy, authzPolicy,
                            proxiedAuthzTemplate, useSubtreeDelete, usePermissiveModify, schema),
            return new LDAPCollectionResourceProvider(baseDN, rootMapper, nameStrategy, etagAttribute,
                    new Config(readOnUpdatePolicy, useSubtreeDelete, usePermissiveModify, schema),
                    additionalLDAPAttributes);
        }
@@ -283,23 +247,6 @@
        }
        /**
         * Sets the LDAP connection factory to be used for accessing the LDAP
         * directory. Each HTTP request will obtain a single connection from the
         * factory and then close it once the HTTP response has been sent. It is
         * recommended that the provided connection factory supports connection
         * pooling.
         *
         * @param factory
         *            The LDAP connection factory to be used for accessing the
         *            LDAP directory.
         * @return A reference to this LDAP resource collection builder.
         */
        public Builder ldapConnectionFactory(final ConnectionFactory factory) {
            this.factory = factory;
            return this;
        }
        /**
         * Sets the attribute mapper which should be used for mapping JSON
         * resources to and from LDAP entries.
         *
@@ -313,23 +260,6 @@
        }
        /**
         * Sets the authorization ID template which will be used for proxied
         * authorization. Template parameters are specified by including the
         * parameter name surrounded by curly braces. The template should
         * contain fields which are expected to be found in the security context
         * create during authentication, e.g. "dn:{dn}" or "u:{id}".
         *
         * @param template
         *            The authorization ID template which will be used for
         *            proxied authorization.
         * @return A reference to this LDAP resource collection builder.
         */
        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. The default read on
         * update policy is to use {@link ReadOnUpdatePolicy#CONTROLS controls}.
@@ -709,23 +639,23 @@
        }
        @Override
        SearchRequest createSearchRequest(final RequestState requestState, final DN baseDN, final String resourceId) {
        SearchRequest createSearchRequest(final Connection connection, final DN baseDN, final String resourceId) {
            return newSearchRequest(baseDN, SearchScope.SINGLE_LEVEL, Filter.equality(idAttribute
                    .toString(), resourceId));
        }
        @Override
        void getLDAPAttributes(final RequestState requestState, final Set<String> ldapAttributes) {
        void getLDAPAttributes(final Connection connection, final Set<String> ldapAttributes) {
            ldapAttributes.add(idAttribute.toString());
        }
        @Override
        String getResourceId(final RequestState requestState, final Entry entry) {
        String getResourceId(final Connection connection, final Entry entry) {
            return entry.parseAttribute(idAttribute).asString();
        }
        @Override
        void setResourceId(final RequestState requestState, final DN baseDN, final String resourceId,
        void setResourceId(final Connection connection, final DN baseDN, final String resourceId,
                final Entry entry) throws ResourceException {
            if (isServerProvided) {
                if (resourceId != null) {
@@ -749,23 +679,23 @@
        }
        @Override
        SearchRequest createSearchRequest(final RequestState requestState, final DN baseDN, final String resourceId) {
        SearchRequest createSearchRequest(final Connection connection, final DN baseDN, final String resourceId) {
            return newSearchRequest(baseDN.child(rdn(resourceId)), SearchScope.BASE_OBJECT, Filter
                    .objectClassPresent());
        }
        @Override
        void getLDAPAttributes(final RequestState requestState, final Set<String> ldapAttributes) {
        void getLDAPAttributes(final Connection connection, final Set<String> ldapAttributes) {
            ldapAttributes.add(attribute.toString());
        }
        @Override
        String getResourceId(final RequestState requestState, final Entry entry) {
        String getResourceId(final Connection connection, final Entry entry) {
            return entry.parseAttribute(attribute).asString();
        }
        @Override
        void setResourceId(final RequestState requestState, final DN baseDN, final String resourceId,
        void setResourceId(final Connection connection, final DN baseDN, final String resourceId,
                final Entry entry) throws ResourceException {
            if (resourceId != null) {
                entry.setName(baseDN.child(rdn(resourceId)));
@@ -929,7 +859,7 @@
     */
    public static ReferenceAttributeMapper reference(final AttributeDescription attribute,
            final DN baseDN, final AttributeDescription primaryKey, final AttributeMapper mapper) {
        return new ReferenceAttributeMapper(attribute, baseDN, primaryKey, mapper);
        return new ReferenceAttributeMapper(Schema.getDefaultSchema(), attribute, baseDN, primaryKey, mapper);
    }
    /**
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAPHttpApplication.java
@@ -11,123 +11,134 @@
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2015 ForgeRock AS.
 * Copyright 2015-2016 ForgeRock AS.
 */
package org.forgerock.opendj.rest2ldap;
import static org.forgerock.http.util.Json.*;
import static org.forgerock.http.util.Json.readJsonLenient;
import static org.forgerock.opendj.rest2ldap.Rest2LDAP.configureConnectionFactory;
import static org.forgerock.util.Utils.*;
import static org.forgerock.util.Reject.checkNotNull;
import static org.forgerock.util.Utils.closeSilently;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.forgerock.http.Filter;
import org.forgerock.http.Handler;
import org.forgerock.http.HttpApplication;
import org.forgerock.http.HttpApplicationException;
import org.forgerock.http.filter.Filters;
import org.forgerock.http.handler.Handlers;
import org.forgerock.http.io.Buffer;
import org.forgerock.http.protocol.Headers;
import org.forgerock.http.protocol.Request;
import org.forgerock.http.protocol.Response;
import org.forgerock.json.JsonValue;
import org.forgerock.json.resource.CollectionResourceProvider;
import org.forgerock.json.resource.RequestHandler;
import org.forgerock.json.resource.Router;
import org.forgerock.json.resource.http.CrestHttp;
import org.forgerock.opendj.ldap.Connection;
import org.forgerock.opendj.ldap.ConnectionFactory;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.LdapException;
import org.forgerock.opendj.ldap.SearchScope;
import org.forgerock.opendj.ldap.schema.Schema;
import org.forgerock.opendj.rest2ldap.authz.AuthenticationStrategy;
import org.forgerock.opendj.rest2ldap.authz.DirectConnectionFilter;
import org.forgerock.opendj.rest2ldap.authz.HttpBasicAuthenticationFilter;
import org.forgerock.opendj.rest2ldap.authz.HttpBasicAuthenticationFilter.CustomHeaderExtractor;
import org.forgerock.opendj.rest2ldap.authz.HttpBasicAuthenticationFilter.HttpBasicExtractor;
import org.forgerock.opendj.rest2ldap.authz.OptionalFilter;
import org.forgerock.opendj.rest2ldap.authz.OptionalFilter.ConditionalFilter;
import org.forgerock.opendj.rest2ldap.authz.ProxiedAuthV2Filter;
import org.forgerock.opendj.rest2ldap.authz.ProxiedAuthV2Filter.IntrospectionAuthzProvider;
import org.forgerock.opendj.rest2ldap.authz.SASLPlainStrategy;
import org.forgerock.opendj.rest2ldap.authz.SearchThenBindStrategy;
import org.forgerock.opendj.rest2ldap.authz.SimpleBindStrategy;
import org.forgerock.services.context.Context;
import org.forgerock.services.context.SecurityContext;
import org.forgerock.util.Factory;
import org.forgerock.util.Reject;
import org.forgerock.util.Function;
import org.forgerock.util.Pair;
import org.forgerock.util.promise.NeverThrowsException;
import org.forgerock.util.promise.Promise;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** Rest2ldap HTTP application. */
public final class Rest2LDAPHttpApplication implements HttpApplication {
public class Rest2LDAPHttpApplication implements HttpApplication {
    private static final String DEFAULT_ROOT_FACTORY = "root";
    private static final String DEFAULT_BIND_FACTORY = "bind";
    private static final Logger LOG = LoggerFactory.getLogger(Rest2LDAPHttpApplication.class);
    private static final class HttpHandler implements Handler, Closeable {
        private final ConnectionFactory ldapConnectionFactory;
        private final Handler delegate;
    /** URL to the JSON configuration file. */
    protected final URL configurationUrl;
        HttpHandler(final JsonValue configuration) {
            ldapConnectionFactory = createLdapConnectionFactory(configuration);
            try {
                delegate = CrestHttp.newHttpHandler(createRouter(configuration, ldapConnectionFactory));
            } catch (final RuntimeException e) {
                closeSilently(ldapConnectionFactory);
                throw e;
            }
        }
    /** Schema used to perform DN validations. */
    protected final Schema schema;
        private static RequestHandler createRouter(
                final JsonValue configuration, final ConnectionFactory ldapConnectionFactory) {
            final AuthorizationPolicy authzPolicy = configuration.get("servlet")
                    .get("authorizationPolicy")
                    .required()
                    .asEnum(AuthorizationPolicy.class);
            final String proxyAuthzTemplate = configuration.get("servlet").get("proxyAuthzIdTemplate").asString();
            final JsonValue mappings = configuration.get("servlet").get("mappings").required();
    private final Map<String, ConnectionFactory> connectionFactories = new HashMap<>();
            final Router router = new Router();
            for (final String mappingUrl : mappings.keys()) {
                final JsonValue mapping = mappings.get(mappingUrl);
                final CollectionResourceProvider provider = Rest2LDAP.builder()
                        .ldapConnectionFactory(ldapConnectionFactory)
                        .authorizationPolicy(authzPolicy)
                        .proxyAuthzIdTemplate(proxyAuthzTemplate)
                        .configureMapping(mapping)
                        .build();
                router.addRoute(Router.uriTemplate(mappingUrl), provider);
            }
            return router;
        }
    private enum Policy {
        oauth2    (0),
        basic     (50),
        anonymous (100);
        private static ConnectionFactory createLdapConnectionFactory(final JsonValue configuration) {
            final String ldapFactoryName = configuration.get("servlet").get("ldapConnectionFactory").asString();
            if (ldapFactoryName != null) {
                return configureConnectionFactory(
                        configuration.get("ldapConnectionFactories").required(), ldapFactoryName);
            }
            return null;
        }
        private final int priority;
        @Override
        public void close() {
            closeSilently(ldapConnectionFactory);
        }
        @Override
        public Promise<Response, NeverThrowsException> handle(final Context context, final Request request) {
            return delegate.handle(context, request);
        Policy(int priority) {
            this.priority = priority;
        }
    }
    private final URL configurationUrl;
    private HttpHandler handler;
    private HttpAuthenticationFilter filter;
    private enum BindStrategy {
        simple, search, sasl_plain
    }
    /**
     * Default constructor called by the HTTP Framework which will use the
     * default configuration file location.
     * Default constructor called by the HTTP Framework which will use the default configuration file location.
     */
    public Rest2LDAPHttpApplication() {
        this.configurationUrl = getClass().getResource("/opendj-rest2ldap-config.json");
        this.schema = Schema.getDefaultSchema();
    }
    /**
     * Creates a new Rest2LDAP HTTP application using the provided configuration URL.
     *
     * @param configurationURL
     *            The URL to the JSON configuration file.
     *            The URL to the JSON configuration file
     * @param schema
     *            The {@link Schema} used to perform DN validations
     */
    public Rest2LDAPHttpApplication(final URL configurationURL) {
        Reject.ifNull(configurationURL, "The configuration URL must not be null");
        this.configurationUrl = configurationURL;
    public Rest2LDAPHttpApplication(final URL configurationURL, final Schema schema) {
        this.configurationUrl = checkNotNull(configurationURL, "configurationURL cannot be null");
        this.schema = checkNotNull(schema, "schema cannot be null");
    }
    @Override
    public final Handler start() throws HttpApplicationException {
        try {
            final JsonValue configuration = readJson(configurationUrl);
            configureConnectionFactories(configuration.get("ldapConnectionFactories"));
            return Handlers.chainOf(
                    CrestHttp.newHttpHandler(configureRest2Ldap(configuration)),
                    newAuthorizationFilter(configuration.get("authorization").required()));
        } catch (final Exception e) {
            // TODO i18n, once supported in opendj-rest2ldap
            final String errorMsg = "Unable to start Rest2Ldap Http Application";
            LOG.error(errorMsg, e);
            stop();
            throw new HttpApplicationException(errorMsg, e);
        }
    }
    private static JsonValue readJson(final URL resource) throws IOException {
@@ -136,19 +147,20 @@
        }
    }
    @Override
    public Handler start() throws HttpApplicationException {
        try {
            final JsonValue configuration = readJson(configurationUrl);
            handler = new HttpHandler(configuration);
            filter = new HttpAuthenticationFilter(configuration);
            return Handlers.chainOf(handler, filter);
        } catch (final Exception e) {
            // TODO i18n, once supported in opendj-rest2ldap
            final String errorMsg = "Unable to start Rest2Ldap Http Application";
            LOG.error(errorMsg, e);
            stop();
            throw new HttpApplicationException(errorMsg, e);
    private static RequestHandler configureRest2Ldap(final JsonValue configuration) {
        final JsonValue mappings = configuration.get("mappings").required();
        final Router router = new Router();
        for (final String mappingUrl : mappings.keys()) {
            final JsonValue mapping = mappings.get(mappingUrl);
            router.addRoute(Router.uriTemplate(mappingUrl), Rest2LDAP.builder().configureMapping(mapping).build());
        }
        return router;
    }
    private void configureConnectionFactories(final JsonValue config) {
        connectionFactories.clear();
        for (String name : config.keys()) {
            connectionFactories.put(name, configureConnectionFactory(config, name));
        }
    }
@@ -160,8 +172,236 @@
    @Override
    public void stop() {
        closeSilently(handler, filter);
        handler = null;
        filter = null;
        for (ConnectionFactory factory : connectionFactories.values()) {
            closeSilently(factory);
        }
        connectionFactories.clear();
    }
    private Filter newAuthorizationFilter(final JsonValue config) {
        final List<Policy> configuredPolicies = new ArrayList<>();
        for (String policy : config.get("policies").required().asList(String.class)) {
            configuredPolicies.add(Policy.valueOf(policy.toLowerCase()));
        }
        final TreeMap<Integer, Filter> policyFilters = new TreeMap<>();
        final int lastIndex = configuredPolicies.size() - 1;
        for (int i = 0; i < configuredPolicies.size(); i++) {
            final Policy policy = configuredPolicies.get(i);
            policyFilters.put(policy.priority,
                    buildAuthzPolicyFilter(policy, config.get(policy.toString()), i != lastIndex));
        }
        return Filters.chainOf(new ArrayList<>(policyFilters.values()));
    }
    private Filter buildAuthzPolicyFilter(final Policy policy, final JsonValue config, boolean optional) {
        switch (policy) {
        case anonymous:
            return buildAnonymousFilter(config);
        case basic:
            final ConditionalFilter basicFilter = buildBasicFilter(config.required());
            final Filter basicFilterChain =
                    config.get("reuseAuthenticatedConnection").defaultTo(Boolean.FALSE).asBoolean()
                        ? basicFilter
                        : Filters.chainOf(basicFilter, newProxyAuthzFilter(getConnectionFactory(DEFAULT_ROOT_FACTORY),
                                                                           IntrospectionAuthzProvider.INSTANCE));
            return optional ? new OptionalFilter(basicFilterChain, basicFilter) : basicFilterChain;
        default:
            throw new IllegalArgumentException("Unsupported policy '" + policy + "'");
        }
    }
    /**
     * Create a new {@link Filter} in charge of injecting {@link AuthenticatedConnectionContext}.
     *
     * @param connectionFactory
     *            The {@link ConnectionFactory} providing the {@link Connection} injected as
     *            {@link AuthenticatedConnectionContext}
     * @param authzIdProvider
     *            Function computing the authzId to use for the LDAP's ProxiedAuth control.
     * @return a newly created {@link Filter}
     */
    protected Filter newProxyAuthzFilter(final ConnectionFactory connectionFactory,
            final Function<SecurityContext, String, LdapException> authzIdProvider) {
        return new ProxiedAuthV2Filter(connectionFactory, authzIdProvider);
    }
    private Filter buildAnonymousFilter(final JsonValue config) {
        if (config.contains("userDN")) {
            final DN userDN = DN.valueOf(config.get("userDN").asString(), schema);
            final Map<String, Object> authz = new HashMap<>(1);
            authz.put(SecurityContext.AUTHZID_DN, userDN.toString());
            return Filters.chainOf(
                    newStaticSecurityContextFilter(null, authz),
                    newProxyAuthzFilter(
                            getConnectionFactory(config.get("ldapConnectionFactory")
                                    .defaultTo(DEFAULT_ROOT_FACTORY)
                                    .asString()),
                            IntrospectionAuthzProvider.INSTANCE));
        }
        return newDirectConnectionFilter(getConnectionFactory(config.get("ldapConnectionFactory")
                                                             .defaultTo(DEFAULT_ROOT_FACTORY).asString()));
    }
    /**
     * Create a new {@link Filter} injecting a predefined {@link SecurityContext}.
     *
     * @param authenticationId
     *            AuthenticationID of the {@link SecurityContext}.
     * @param authorization
     *            Authorization of the {@link SecurityContext}
     * @return a newly created {@link Filter}
     */
    protected Filter newStaticSecurityContextFilter(final String authenticationId,
            final Map<String, Object> authorization) {
        return new Filter() {
            @Override
            public Promise<Response, NeverThrowsException> filter(Context context, Request request, Handler next) {
                return next.handle(new SecurityContext(context, authenticationId, authorization), request);
            }
        };
    }
    /**
     * Create a new {@link Filter} in charge of injecting {@link AuthenticatedConnectionContext} directly from a
     * {@link ConnectionFactory}.
     *
     * @param connectionFactory
     *            The {@link ConnectionFactory} used to get the {@link Connection}
     * @return a newly created {@link Filter}
     */
    protected Filter newDirectConnectionFilter(ConnectionFactory connectionFactory) {
        return new DirectConnectionFilter(connectionFactory);
    }
    /**
     * Get a {@link ConnectionFactory} from its name.
     *
     * @param name
     *            Name of the {@link ConnectionFactory} as specified in the configuration
     * @return The associated {@link ConnectionFactory} or null if none can be found
     */
    protected ConnectionFactory getConnectionFactory(final String name) {
        return connectionFactories.get(name);
    }
    private ConditionalFilter buildBasicFilter(final JsonValue config) {
        final String bind = config.get("bind").required().asString();
        final BindStrategy strategy = BindStrategy.valueOf(bind.toLowerCase().replace('-', '_'));
        return newBasicAuthenticationFilter(buildBindStrategy(strategy, config.get(bind).required()),
                config.get("supportAltAuthentication").defaultTo(Boolean.FALSE).asBoolean()
                        ? new CustomHeaderExtractor(
                                config.get("altAuthenticationUsernameHeader").required().asString(),
                                config.get("altAuthenticationPasswordHeader").required().asString())
                        : HttpBasicExtractor.INSTANCE,
                config.get("reuseAuthenticatedConnection").defaultTo(Boolean.FALSE).asBoolean());
    }
    /**
     * Get a {@link Filter} in charge of performing the HTTP-Basic Authentication. This filter create a
     * {@link SecurityContext} reflecting the authenticated users.
     *
     * @param authenticationStrategy
     *            The {@link AuthenticationStrategy} to use to authenticate the user.
     * @param credentialsExtractor
     *            Extract the user's credentials from the {@link Headers}.
     * @param reuseAuthenticatedConnection
     *            Let the bound connection open so that it can be reused to perform the LDAP operations.
     * @return A new {@link Filter}
     */
    protected ConditionalFilter newBasicAuthenticationFilter(AuthenticationStrategy authenticationStrategy,
            Function<Headers, Pair<String, String>, NeverThrowsException> credentialsExtractor,
            boolean reuseAuthenticatedConnection) {
        return new HttpBasicAuthenticationFilter(authenticationStrategy, credentialsExtractor,
                reuseAuthenticatedConnection);
    }
    private AuthenticationStrategy buildBindStrategy(final BindStrategy strategy, final JsonValue config) {
        switch (strategy) {
        case simple:
            return buildSimpleBindStrategy(config);
        case search:
            return buildSearchThenBindStrategy(config);
        case sasl_plain:
            return buildSASLBindStrategy(config);
        default:
            throw new IllegalArgumentException("Unsupported strategy '" + strategy + "'");
        }
    }
    private AuthenticationStrategy buildSimpleBindStrategy(final JsonValue config) {
        return newSimpleBindStrategy(getConnectionFactory(config.get("ldapConnectionFactory")
                                                                .defaultTo(DEFAULT_BIND_FACTORY).asString()),
                                     config.get("bindDNTemplate").defaultTo("%s").asString(),
                                     schema);
    }
    /**
     * {@link AuthenticationStrategy} performing an LDAP Bind request with a computed DN.
     *
     * @param connectionFactory
     *            The {@link ConnectionFactory} to use to perform the bind operation
     * @param schema
     *            {@link Schema} used to perform the DN validation.
     * @param bindDNTemplate
     *            DN template containing a single %s which will be replaced by the authenticating user's name. (i.e:
     *            uid=%s,ou=people,dc=example,dc=com)
     * @return A new {@link AuthenticationStrategy}
     */
    protected AuthenticationStrategy newSimpleBindStrategy(ConnectionFactory connectionFactory, String bindDNTemplate,
            Schema schema) {
        return new SimpleBindStrategy(connectionFactory, bindDNTemplate, schema);
    }
    private AuthenticationStrategy buildSASLBindStrategy(JsonValue config) {
        return newSASLBindStrategy(getConnectionFactory(config.get("ldapConnectionFactory")
                                                              .defaultTo(DEFAULT_BIND_FACTORY).asString()),
                                   config.get("authcIdTemplate").defaultTo("u:%s").asString());
    }
    /**
     * {@link AuthenticationStrategy} performing an LDAP SASL-Plain Bind.
     *
     * @param connectionFactory
     *            The {@link ConnectionFactory} to use to perform the bind operation
     * @param authcIdTemplate
     *            Authentication identity template containing a single %s which will be replaced by the authenticating
     *            user's name. (i.e: (u:%s)
     * @return A new {@link AuthenticationStrategy}
     */
    protected AuthenticationStrategy newSASLBindStrategy(ConnectionFactory connectionFactory, String authcIdTemplate) {
        return new SASLPlainStrategy(connectionFactory, schema, authcIdTemplate);
    }
    private AuthenticationStrategy buildSearchThenBindStrategy(JsonValue config) {
        return newSearchThenBindStrategy(
                getConnectionFactory(
                        config.get("searchLDAPConnectionFactory").defaultTo(DEFAULT_ROOT_FACTORY).asString()),
                getConnectionFactory(
                        config.get("bindLDAPConnectionFactory").defaultTo(DEFAULT_BIND_FACTORY).asString()),
                DN.valueOf(config.get("baseDN").required().asString(), schema),
                SearchScope.valueOf(config.get("scope").required().asString().toLowerCase()),
                config.get("filterTemplate").required().asString());
    }
    /**
     * {@link AuthenticationStrategy} performing an LDAP Search to get a DN to bind with.
     *
     * @param searchConnectionFactory
     *            The {@link ConnectionFactory} to sue to perform the search operation.
     * @param bindConnectionFactory
     *            The {@link ConnectionFactory} to use to perform the bind operation
     * @param baseDN
     *            The base DN of the search request
     * @param scope
     *            {@link SearchScope} of the search request
     * @param filterTemplate
     *            filter template containing a single %s which will be replaced by the authenticating user's name. (i.e:
     *            (&(uid=%s)(objectClass=inetOrgPerson))
     * @return A new {@link AuthenticationStrategy}
     */
    protected AuthenticationStrategy newSearchThenBindStrategy(ConnectionFactory searchConnectionFactory,
            ConnectionFactory bindConnectionFactory, DN baseDN, SearchScope scope, String filterTemplate) {
        return new SearchThenBindStrategy(
                searchConnectionFactory, bindConnectionFactory, baseDN, scope, filterTemplate);
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java
@@ -11,7 +11,7 @@
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2012-2015 ForgeRock AS.
 * Copyright 2012-2016 ForgeRock AS.
 */
package org.forgerock.opendj.rest2ldap;
@@ -26,6 +26,7 @@
import org.forgerock.opendj.ldap.Attribute;
import org.forgerock.opendj.ldap.AttributeDescription;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.Connection;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.util.Function;
@@ -114,7 +115,7 @@
    }
    @Override
    Promise<Filter, ResourceException> getLDAPFilter(final RequestState requestState, final JsonPointer path,
    Promise<Filter, ResourceException> getLDAPFilter(final Connection connection, final JsonPointer path,
            final JsonPointer subPath, final FilterType type, final String operator, final Object valueAssertion) {
        if (subPath.isEmpty()) {
            try {
@@ -136,7 +137,7 @@
    @Override
    Promise<Attribute, ResourceException> getNewLDAPAttributes(
            final RequestState requestState, final JsonPointer path, final List<Object> newValues) {
            final Connection connection, final JsonPointer path, final List<Object> newValues) {
        try {
            return newResultPromise(jsonToAttribute(newValues, ldapAttributeName, encoder()));
        } catch (final Exception ex) {
@@ -152,7 +153,7 @@
    }
    @Override
    Promise<JsonValue, ResourceException> read(final RequestState requestState, final JsonPointer path, final Entry e) {
    Promise<JsonValue, ResourceException> read(final Connection connection, final JsonPointer path, final Entry e) {
        try {
            final Object value;
            if (attributeIsSingleValued()) {
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/AbstractAsynchronousConnectionDecorator.java
New file
@@ -0,0 +1,130 @@
/*
 * 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 2016 ForgeRock AS.
 */
package org.forgerock.opendj.rest2ldap.authz;
import static org.forgerock.util.Reject.*;
import org.forgerock.opendj.ldap.AbstractAsynchronousConnection;
import org.forgerock.opendj.ldap.Connection;
import org.forgerock.opendj.ldap.ConnectionEventListener;
import org.forgerock.opendj.ldap.IntermediateResponseHandler;
import org.forgerock.opendj.ldap.LdapPromise;
import org.forgerock.opendj.ldap.SearchResultHandler;
import org.forgerock.opendj.ldap.requests.AbandonRequest;
import org.forgerock.opendj.ldap.requests.AddRequest;
import org.forgerock.opendj.ldap.requests.BindRequest;
import org.forgerock.opendj.ldap.requests.CompareRequest;
import org.forgerock.opendj.ldap.requests.DeleteRequest;
import org.forgerock.opendj.ldap.requests.ExtendedRequest;
import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
import org.forgerock.opendj.ldap.requests.ModifyRequest;
import org.forgerock.opendj.ldap.requests.SearchRequest;
import org.forgerock.opendj.ldap.requests.UnbindRequest;
import org.forgerock.opendj.ldap.responses.BindResult;
import org.forgerock.opendj.ldap.responses.CompareResult;
import org.forgerock.opendj.ldap.responses.ExtendedResult;
import org.forgerock.opendj.ldap.responses.Result;
abstract class AbstractAsynchronousConnectionDecorator extends AbstractAsynchronousConnection {
    protected final Connection delegate;
    AbstractAsynchronousConnectionDecorator(Connection delegate) {
        this.delegate = checkNotNull(delegate, "delegate cannot be null");
    }
    @Override
    public LdapPromise<Void> abandonAsync(AbandonRequest request) {
        return delegate.abandonAsync(request);
    }
    @Override
    public LdapPromise<Result> addAsync(AddRequest request, IntermediateResponseHandler intermediateResponseHandler) {
        return delegate.addAsync(request, intermediateResponseHandler);
    }
    @Override
    public void addConnectionEventListener(ConnectionEventListener listener) {
        delegate.addConnectionEventListener(listener);
    }
    @Override
    public LdapPromise<BindResult> bindAsync(BindRequest request,
            IntermediateResponseHandler intermediateResponseHandler) {
        return delegate.bindAsync(request, intermediateResponseHandler);
    }
    @Override
    public void close(UnbindRequest request, String reason) {
        delegate.close(request, reason);
    }
    @Override
    public LdapPromise<CompareResult> compareAsync(CompareRequest request,
            IntermediateResponseHandler intermediateResponseHandler) {
        return delegate.compareAsync(request, intermediateResponseHandler);
    }
    @Override
    public LdapPromise<Result> deleteAsync(DeleteRequest request,
            IntermediateResponseHandler intermediateResponseHandler) {
        return delegate.deleteAsync(request, intermediateResponseHandler);
    }
    @Override
    public <R extends ExtendedResult> LdapPromise<R> extendedRequestAsync(ExtendedRequest<R> request,
            IntermediateResponseHandler intermediateResponseHandler) {
        return delegate.extendedRequestAsync(request, intermediateResponseHandler);
    }
    @Override
    public boolean isClosed() {
        return delegate.isClosed();
    }
    @Override
    public boolean isValid() {
        return delegate.isValid();
    }
    @Override
    public LdapPromise<Result> modifyAsync(ModifyRequest request,
            IntermediateResponseHandler intermediateResponseHandler) {
        return delegate.modifyAsync(request, intermediateResponseHandler);
    }
    @Override
    public LdapPromise<Result> modifyDNAsync(ModifyDNRequest request,
            IntermediateResponseHandler intermediateResponseHandler) {
        return delegate.modifyDNAsync(request, intermediateResponseHandler);
    }
    @Override
    public void removeConnectionEventListener(ConnectionEventListener listener) {
        delegate.removeConnectionEventListener(listener);
    }
    @Override
    public LdapPromise<Result> searchAsync(SearchRequest request,
            IntermediateResponseHandler intermediateResponseHandler, SearchResultHandler entryHandler) {
        return delegate.searchAsync(request, intermediateResponseHandler, entryHandler);
    }
    @Override
    public String toString() {
        return delegate.toString();
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/AuthenticationStrategy.java
New file
@@ -0,0 +1,44 @@
/*
 * 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 2016 ForgeRock AS.
 */
package org.forgerock.opendj.rest2ldap.authz;
import java.util.concurrent.atomic.AtomicReference;
import org.forgerock.opendj.ldap.Connection;
import org.forgerock.opendj.ldap.LdapException;
import org.forgerock.services.context.Context;
import org.forgerock.services.context.SecurityContext;
import org.forgerock.util.promise.Promise;
/** Authenticate a user and create a {@link SecurityContext} as a result. */
public interface AuthenticationStrategy {
    /**
     * Authenticate a user.
     *
     * @param username
     *            User to authenticate.
     * @param password
     *            Password used to perform the authentication.
     * @param parentContext
     *            Context to use as parent for the created {@link SecurityContext}
     * @param authenticatedConnectionHolder
     *            Output parameter. If supported, the implementations will set the reference to a ready to be used LDAP
     *            connection bound to the given credentials.
     * @return A {@link Context} if the authentication succeed or an {@link LdapException} otherwise.
     */
    Promise<SecurityContext, LdapException> authenticate(String username, String password, Context parentContext,
            AtomicReference<Connection> authenticatedConnectionHolder);
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/AuthzIdTemplate.java
File was renamed from opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthzIdTemplate.java
@@ -11,12 +11,9 @@
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2013-2015 ForgeRock AS.
 * Copyright 2013-2016 ForgeRock AS.
 */
package org.forgerock.opendj.rest2ldap;
import static org.forgerock.opendj.rest2ldap.Utils.i18n;
import static org.forgerock.opendj.rest2ldap.Utils.isJSONPrimitive;
package org.forgerock.opendj.rest2ldap.authz;
import java.util.ArrayList;
import java.util.List;
@@ -25,8 +22,6 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.forgerock.json.resource.ForbiddenException;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.schema.Schema;
@@ -38,23 +33,22 @@
 */
final class AuthzIdTemplate {
    private static interface Impl {
        String formatAsAuthzId(AuthzIdTemplate t, Object[] templateVariables, Schema schema)
                throws ResourceException;
        String formatAsAuthzId(AuthzIdTemplate t, Object[] templateVariables, Schema schema);
    }
    private static final Impl DN_IMPL = new Impl() {
        @Override
        public String formatAsAuthzId(final AuthzIdTemplate t, final Object[] templateVariables,
                final Schema schema) throws ResourceException {
                final Schema schema) {
            final String authzId = String.format(Locale.ENGLISH, t.formatString, templateVariables);
            try {
                // Validate the DN.
                DN.valueOf(authzId.substring(3), schema);
            } catch (final IllegalArgumentException e) {
                throw new ForbiddenException(
                        i18n("The request could not be authorized because the required "
                                + "security principal was not a valid LDAP DN"));
                throw new IllegalArgumentException(
                        "The request could not be authorized because the required security principal "
                        + "was not a valid LDAP DN");
            }
            return authzId;
        }
@@ -66,7 +60,7 @@
        @Override
        public String formatAsAuthzId(final AuthzIdTemplate t, final Object[] templateVariables,
                final Schema schema) throws ResourceException {
                final Schema schema) {
            return "dn:" + DN.format(t.dnFormatString, schema, templateVariables);
        }
@@ -78,7 +72,7 @@
        @Override
        public String formatAsAuthzId(final AuthzIdTemplate t, final Object[] templateVariables,
                final Schema schema) throws ResourceException {
                final Schema schema) {
            return String.format(Locale.ENGLISH, t.formatString, templateVariables);
        }
@@ -90,7 +84,15 @@
    private final Impl pimpl;
    private final String template;
    AuthzIdTemplate(final String template) {
    /**
     * Create a new authorization ID template.
     *
     * @param template
     *            Authorization ID template
     * @throws IllegalArgumentException
     *             if template doesn't start with "u:" or "dn:"
     */
    public AuthzIdTemplate(final String template) {
        if (!template.startsWith("u:") && !template.startsWith("dn:")) {
            throw new IllegalArgumentException("Invalid authorization ID template: " + template);
        }
@@ -120,14 +122,21 @@
        return template;
    }
    String formatAsAuthzId(final Map<String, Object> principals, final Schema schema)
            throws ResourceException {
    /**
     * Return the template with all the variable replaced.
     *
     * @param principals
     *            Value to use to replace the variables.
     * @param schema
     *            Schema to perform validation.
     * @return The template with all the variable replaced.
     */
    public String formatAsAuthzId(final Map<String, Object> principals, final Schema schema) {
        final String[] templateVariables = getPrincipalsForFormatting(principals);
        return pimpl.formatAsAuthzId(this, templateVariables, schema);
    }
    private String[] getPrincipalsForFormatting(final Map<String, Object> principals)
            throws ForbiddenException {
    private String[] getPrincipalsForFormatting(final Map<String, Object> principals) {
        final String[] values = new String[keys.size()];
        for (int i = 0; i < values.length; i++) {
            final String key = keys.get(i);
@@ -135,15 +144,19 @@
            if (isJSONPrimitive(value)) {
                values[i] = String.valueOf(value);
            } else if (value != null) {
                throw new ForbiddenException(i18n(
                throw new IllegalArgumentException(String.format(
                        "The request could not be authorized because the required "
                                + "security principal '%s' had an invalid data type", key));
            } else {
                throw new ForbiddenException(i18n(
                throw new IllegalArgumentException(String.format(
                        "The request could not be authorized because the required "
                                + "security principal '%s' could not be determined", key));
            }
        }
        return values;
    }
    static boolean isJSONPrimitive(final Object value) {
        return value instanceof String || value instanceof Boolean || value instanceof Number;
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/CachedReadConnectionDecorator.java
New file
@@ -0,0 +1,258 @@
/*
 * 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 2016 ForgeRock AS.
 */
package org.forgerock.opendj.rest2ldap.authz;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import org.forgerock.opendj.ldap.Connection;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.IntermediateResponseHandler;
import org.forgerock.opendj.ldap.LdapException;
import org.forgerock.opendj.ldap.LdapPromise;
import org.forgerock.opendj.ldap.LdapResultHandler;
import org.forgerock.opendj.ldap.SearchResultHandler;
import org.forgerock.opendj.ldap.SearchScope;
import org.forgerock.opendj.ldap.controls.SubtreeDeleteRequestControl;
import org.forgerock.opendj.ldap.requests.BindRequest;
import org.forgerock.opendj.ldap.requests.DeleteRequest;
import org.forgerock.opendj.ldap.requests.ExtendedRequest;
import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
import org.forgerock.opendj.ldap.requests.ModifyRequest;
import org.forgerock.opendj.ldap.requests.SearchRequest;
import org.forgerock.opendj.ldap.responses.BindResult;
import org.forgerock.opendj.ldap.responses.ExtendedResult;
import org.forgerock.opendj.ldap.responses.Result;
import org.forgerock.opendj.ldap.responses.SearchResultEntry;
import org.forgerock.opendj.ldap.responses.SearchResultReference;
/**
 * Cache entries by intercepting the result of base search requests. Entries in cache are automatically evicted if their
 * DN is involved in a modify/delete operation. This cache is used to prevent multiple read operations on the same DN.
 * This happens frequently when we have to resolve entry references in a collection.
 */
final class CachedReadConnectionDecorator extends AbstractAsynchronousConnectionDecorator {
    @SuppressWarnings("serial")
    private final Map<DN, CachedRead> cachedReads = new LinkedHashMap<DN, CachedRead>() {
        private static final int MAX_CACHED_ENTRIES = 32;
        @Override
        protected boolean removeEldestEntry(final Map.Entry<DN, CachedRead> eldest) {
            return size() > MAX_CACHED_ENTRIES;
        }
    };
    /** A cached read request - see cachedReads for more information. */
    private static final class CachedRead implements SearchResultHandler, LdapResultHandler<Result> {
        private SearchResultEntry cachedEntry;
        private final String cachedFilterString;
        /** Promise of the pending read operation. @GuardedBy("cachedPromiseLatch"). */
        private LdapPromise<Result> cachedPromise;
        private final CountDownLatch cachedPromiseLatch = new CountDownLatch(1);
        private final SearchRequest cachedRequest;
        private volatile Result cachedResult;
        private final ConcurrentLinkedQueue<SearchResultHandler> waitingResultHandlers = new ConcurrentLinkedQueue<>();
        CachedRead(final SearchRequest request, final SearchResultHandler resultHandler) {
            this.cachedRequest = request;
            this.cachedFilterString = request.getFilter().toString();
            this.waitingResultHandlers.add(resultHandler);
        }
        @Override
        public boolean handleEntry(final SearchResultEntry entry) {
            cachedEntry = entry;
            return true;
        }
        @Override
        public void handleException(final LdapException exception) {
            handleResult(exception.getResult());
        }
        @Override
        public boolean handleReference(final SearchResultReference reference) {
            // Ignore - should never happen for a base object search.
            return true;
        }
        @Override
        public void handleResult(final Result result) {
            cachedResult = result;
            drainQueue();
        }
        void addResultHandler(final SearchResultHandler resultHandler) {
            // Fast path.
            if (cachedResult != null) {
                invokeResultHandler(resultHandler);
                return;
            }
            // Enqueue and re-check.
            waitingResultHandlers.add(resultHandler);
            if (cachedResult != null) {
                drainQueue();
            }
        }
        LdapPromise<Result> getPromise() {
            // Perform uninterrupted wait since this method is unlikely to block for a long time.
            boolean wasInterrupted = false;
            while (true) {
                try {
                    cachedPromiseLatch.await();
                    if (wasInterrupted) {
                        Thread.currentThread().interrupt();
                    }
                    return cachedPromise;
                } catch (final InterruptedException e) {
                    wasInterrupted = true;
                }
            }
        }
        boolean isMatchingRead(final SearchRequest request) {
            // Cached reads are always base object.
            return request.getScope().equals(SearchScope.BASE_OBJECT)
                    // Filters must match.
                    && request.getFilter().toString().equals(cachedFilterString)
                    // List of requested attributes must match.
                    && request.getAttributes().equals(cachedRequest.getAttributes());
        }
        void setPromise(final LdapPromise<Result> promise) {
            cachedPromise = promise;
            cachedPromiseLatch.countDown();
        }
        private void drainQueue() {
            SearchResultHandler resultHandler;
            while ((resultHandler = waitingResultHandlers.poll()) != null) {
                invokeResultHandler(resultHandler);
            }
        }
        private void invokeResultHandler(final SearchResultHandler searchResultHandler) {
            if (cachedEntry != null) {
                searchResultHandler.handleEntry(cachedEntry);
            }
        }
    }
    CachedReadConnectionDecorator(Connection delegate) {
        super(delegate);
    }
    @Override
    public LdapPromise<BindResult> bindAsync(BindRequest request,
            IntermediateResponseHandler intermediateResponseHandler) {
        /*
         * Simple brute force implementation in case the bind operation
         * modifies an entry: clear the cachedReads.
         */
        evictAll();
        return delegate.bindAsync(request, intermediateResponseHandler);
    }
    private void evictAll() {
        synchronized (cachedReads) {
            cachedReads.clear();
        }
    }
    @Override
    public <R extends ExtendedResult> LdapPromise<R> extendedRequestAsync(ExtendedRequest<R> request,
            IntermediateResponseHandler intermediateResponseHandler) {
        /*
         * Simple brute force implementation in case the extended
         * operation modifies an entry: clear the cachedReads.
         */
        evictAll();
        return delegate.extendedRequestAsync(request, intermediateResponseHandler);
    }
    @Override
    public LdapPromise<Result> modifyAsync(ModifyRequest request,
            IntermediateResponseHandler intermediateResponseHandler) {
        evict(request.getName());
        return delegate.modifyAsync(request, intermediateResponseHandler);
    }
    private void evict(final DN name) {
        synchronized (cachedReads) {
            cachedReads.remove(name);
        }
    }
    @Override
    public LdapPromise<Result> deleteAsync(DeleteRequest request,
            IntermediateResponseHandler intermediateResponseHandler) {
        // Simple brute force implementation: clear the cachedReads.
        if (request.containsControl(SubtreeDeleteRequestControl.OID)) {
            evictAll();
        } else {
            evict(request.getName());
        }
        return delegate.deleteAsync(request, intermediateResponseHandler);
    }
    @Override
    public LdapPromise<Result> modifyDNAsync(ModifyDNRequest request,
            IntermediateResponseHandler intermediateResponseHandler) {
        // Simple brute force implementation: clear the cachedReads.
        evictAll();
        return delegate.modifyDNAsync(request, intermediateResponseHandler);
    }
    @Override
    public LdapPromise<Result> searchAsync(SearchRequest request,
            IntermediateResponseHandler intermediateResponseHandler, SearchResultHandler entryHandler) {
        /*
         * Don't attempt caching if this search is not a read (base
         * object), or if the search request passed in an intermediate
         * response handler.
         */
        if (!request.getScope().equals(SearchScope.BASE_OBJECT) || intermediateResponseHandler != null) {
            return delegate.searchAsync(request, intermediateResponseHandler, entryHandler);
        }
        // This is a read request and a candidate for caching.
        final CachedRead cachedRead;
        synchronized (cachedReads) {
            cachedRead = cachedReads.get(request.getName());
        }
        if (cachedRead != null && cachedRead.isMatchingRead(request)) {
            // The cached read matches this read request.
            cachedRead.addResultHandler(entryHandler);
            return cachedRead.getPromise();
        } else {
            // Cache the read, possibly evicting a non-matching cached read.
            final CachedRead pendingCachedRead = new CachedRead(request, entryHandler);
            synchronized (cachedReads) {
                cachedReads.put(request.getName(), pendingCachedRead);
            }
            final LdapPromise<Result> promise = delegate
                    .searchAsync(request, intermediateResponseHandler, pendingCachedRead)
                    .thenOnResult(pendingCachedRead).thenOnException(pendingCachedRead);
            pendingCachedRead.setPromise(promise);
            return promise;
        }
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/DirectConnectionFilter.java
New file
@@ -0,0 +1,75 @@
/*
 * 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 2016 ForgeRock AS.
 */
package org.forgerock.opendj.rest2ldap.authz;
import static org.forgerock.opendj.rest2ldap.authz.Utils.*;
import static org.forgerock.util.Reject.*;
import java.util.concurrent.atomic.AtomicReference;
import org.forgerock.http.Filter;
import org.forgerock.http.Handler;
import org.forgerock.http.protocol.Request;
import org.forgerock.http.protocol.Response;
import org.forgerock.opendj.ldap.Connection;
import org.forgerock.opendj.ldap.ConnectionFactory;
import org.forgerock.opendj.ldap.LdapException;
import org.forgerock.opendj.rest2ldap.AuthenticatedConnectionContext;
import org.forgerock.services.context.Context;
import org.forgerock.util.AsyncFunction;
import org.forgerock.util.promise.NeverThrowsException;
import org.forgerock.util.promise.Promise;
/**
 * Inject {@link Connection} into a {@link AuthenticatedConnectionContext}.
 */
public final class DirectConnectionFilter implements Filter {
    private final ConnectionFactory connectionFactory;
    /**
     * Create a new {@link DirectConnectionFilter}.
     *
     * @param connectionFactory
     *            The factory used to get the {@link Connection}
     * @throws NullPointerException
     *             if connectionFactory is null.
     */
    public DirectConnectionFilter(ConnectionFactory connectionFactory) {
        this.connectionFactory = checkNotNull(connectionFactory, "connectionFactory cannot be null");
    }
    @Override
    public Promise<Response, NeverThrowsException> filter(final Context context, final Request request,
            final Handler next) {
        final AtomicReference<Connection> connectionHolder = new AtomicReference<Connection>();
        return connectionFactory
                .getConnectionAsync()
                .thenAsync(new AsyncFunction<Connection, Response, NeverThrowsException>() {
                    @Override
                    public Promise<Response, NeverThrowsException> apply(Connection connection) {
                        connectionHolder.set(connection);
                        return next.handle(new AuthenticatedConnectionContext(context, connection), request);
                    }
                }, new AsyncFunction<LdapException, Response, NeverThrowsException>() {
                    @Override
                    public Promise<Response, NeverThrowsException> apply(LdapException exception) {
                        return asErrorResponse(exception);
                    }
                })
                .thenFinally(close(connectionHolder));
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/HttpBasicAuthenticationFilter.java
New file
@@ -0,0 +1,184 @@
/*
 * 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 2016 ForgeRock AS.
 */
package org.forgerock.opendj.rest2ldap.authz;
import static org.forgerock.opendj.rest2ldap.authz.Utils.asErrorResponse;
import static org.forgerock.util.Reject.checkNotNull;
import static org.forgerock.opendj.rest2ldap.authz.Utils.close;
import java.util.concurrent.atomic.AtomicReference;
import org.forgerock.http.Handler;
import org.forgerock.http.protocol.Headers;
import org.forgerock.http.protocol.Request;
import org.forgerock.http.protocol.Response;
import org.forgerock.opendj.ldap.Connection;
import org.forgerock.opendj.ldap.EntryNotFoundException;
import org.forgerock.opendj.ldap.LdapException;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.rest2ldap.AuthenticatedConnectionContext;
import org.forgerock.opendj.rest2ldap.authz.OptionalFilter.ConditionalFilter;
import org.forgerock.services.context.Context;
import org.forgerock.services.context.SecurityContext;
import org.forgerock.util.AsyncFunction;
import org.forgerock.util.Function;
import org.forgerock.util.Pair;
import org.forgerock.util.encode.Base64;
import org.forgerock.util.promise.NeverThrowsException;
import org.forgerock.util.promise.Promise;
/**
 * Inject a {@link SecurityContext} if the credentials provided in the {@link Request} headers have been successfully
 * verified.
 */
public final class HttpBasicAuthenticationFilter implements ConditionalFilter {
    private final AuthenticationStrategy authenticationStrategy;
    private final Function<Headers, Pair<String, String>, NeverThrowsException> credentialsExtractor;
    private final boolean reuseAuthenticatedConnection;
    /**
     * Create a new HttpBasicAuthenticationFilter.
     *
     * @param authenticationStrategy
     *            The strategy to use to perform the authentication.
     * @param credentialsExtractor
     *            The function to use to extract credentials from the {@link Headers}.
     * @param reuseAuthenticatedConnection
     *            Let the bound connection open so that it can be reused to perform the LDAP operations.
     * @throws NullPointerException
     *             If a parameter is null.
     */
    public HttpBasicAuthenticationFilter(AuthenticationStrategy authenticationStrategy,
            Function<Headers, Pair<String, String>, NeverThrowsException> credentialsExtractor,
            boolean reuseAuthenticatedConnection) {
        this.authenticationStrategy = checkNotNull(authenticationStrategy, "authenticationStrategy cannot be null");
        this.credentialsExtractor = checkNotNull(credentialsExtractor, "credentialsExtractor cannot be null");
        this.reuseAuthenticatedConnection = reuseAuthenticatedConnection;
    }
    @Override
    public boolean canApplyFilter(Context context, Request request) {
        return credentialsExtractor.apply(request.getHeaders()) != null;
    }
    @Override
    public Promise<Response, NeverThrowsException> filter(final Context context, final Request request,
            final Handler next) {
        final Pair<String, String> credentials = credentialsExtractor.apply(request.getHeaders());
        if (credentials == null) {
            return asErrorResponse(LdapException.newLdapException(ResultCode.INVALID_CREDENTIALS));
        }
        final AtomicReference<Connection> authConnHolder = new AtomicReference<Connection>();
        return authenticationStrategy
                .authenticate(credentials.getFirst(), credentials.getSecond(), context, authConnHolder)
                .thenAsync(new AsyncFunction<SecurityContext, Response, NeverThrowsException>() {
                    @Override
                    public Promise<Response, NeverThrowsException> apply(final SecurityContext securityContext) {
                        if (reuseAuthenticatedConnection) {
                            return next
                                    .handle(new AuthenticatedConnectionContext(securityContext, authConnHolder.get()),
                                            request);
                        }
                        close(authConnHolder);
                        return next.handle(securityContext, request);
                    }
                }, new AsyncFunction<LdapException, Response, NeverThrowsException>() {
                    @Override
                    public Promise<? extends Response, ? extends NeverThrowsException> apply(LdapException exception) {
                        return asErrorResponse(exception instanceof EntryNotFoundException
                                ? LdapException.newLdapException(ResultCode.INVALID_CREDENTIALS, exception.getMessage())
                                : exception);
                    }
                })
                .thenFinally(close(authConnHolder));
    }
    /** Extract the user's credentials from custom {@link Headers}. */
    public static final class CustomHeaderExtractor
            implements Function<Headers, Pair<String, String>, NeverThrowsException> {
        private final String customHeaderUsername;
        private final String customHeaderPassword;
        /**
         * Create a new CustomHeaderExtractor.
         *
         * @param customHeaderUsername
         *            Name of the header containing the username
         * @param customHeaderPassword
         *            Name of the header containing the password
         * @throws NullPointerException
         *             if a parameter is null.
         */
        public CustomHeaderExtractor(String customHeaderUsername, String customHeaderPassword) {
            this.customHeaderUsername = checkNotNull(customHeaderUsername, "customHeaderUsername cannot be null");
            this.customHeaderPassword = checkNotNull(customHeaderPassword, "customHeaderPassword cannot be null");
        }
        @Override
        public Pair<String, String> apply(Headers headers) {
            final String userName = headers.getFirst(customHeaderUsername);
            final String password = headers.getFirst(customHeaderPassword);
            if (userName != null && password != null) {
                return Pair.of(userName, password);
            }
            return HttpBasicExtractor.INSTANCE.apply(headers);
        }
    }
    /** Extract the user's credentials from the standard HTTP Basic {@link Headers}. */
    public static final class HttpBasicExtractor
            implements Function<Headers, Pair<String, String>, NeverThrowsException> {
        /** HTTP Header sent by the client with HTTP basic authentication. */
        public static final String HTTP_BASIC_AUTH_HEADER = "Authorization";
        /** Reference to the HttpBasicExtractor Singleton. */
        public static final HttpBasicExtractor INSTANCE = new HttpBasicExtractor();
        private HttpBasicExtractor() { }
        @Override
        public Pair<String, String> apply(Headers headers) {
            final String httpBasicAuthHeader = headers.getFirst(HTTP_BASIC_AUTH_HEADER);
            if (httpBasicAuthHeader != null) {
                final Pair<String, String> userCredentials = parseUsernamePassword(httpBasicAuthHeader);
                if (userCredentials != null) {
                    return userCredentials;
                }
            }
            return null;
        }
        private Pair<String, String> parseUsernamePassword(String authHeader) {
            if (authHeader != null && (authHeader.toLowerCase().startsWith("basic"))) {
                // We received authentication info
                // Example received header:
                // "Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="
                final String base64UserCredentials = authHeader.substring("basic".length() + 1);
                // Example usage of base64:
                // Base64("Aladdin:open sesame") = "QWxhZGRpbjpvcGVuIHNlc2FtZQ=="
                final String userCredentials = new String(Base64.decode(base64UserCredentials));
                String[] split = userCredentials.split(":");
                if (split.length == 2) {
                    return Pair.of(split[0], split[1]);
                }
            }
            return null;
        }
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/OptionalFilter.java
New file
@@ -0,0 +1,71 @@
/*
 * 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 2016 ForgeRock AS.
 */
package org.forgerock.opendj.rest2ldap.authz;
import org.forgerock.http.Filter;
import org.forgerock.http.Handler;
import org.forgerock.http.protocol.Request;
import org.forgerock.http.protocol.Response;
import org.forgerock.services.context.Context;
import org.forgerock.util.promise.NeverThrowsException;
import org.forgerock.util.promise.Promise;
/** Encapsulate a {@link Filter} which can be conditionally applied. */
public final class OptionalFilter implements Filter {
    /** Condition which have to be fulfilled in order to apply the {@link Filter}. */
    public interface Condition {
        /**
         * Check if a {@link Filter} must be executed or not.
         *
         * @param context
         *            Current {@link Context} of the request processing.
         * @param request
         *            the {@link Request} currently processed.
         * @return true if the filter must be applied.
         */
        boolean canApplyFilter(Context context, Request request);
    }
    /** A {@link Filter} which can be conditionally applied. */
    public interface ConditionalFilter extends Filter, Condition {
    }
    private final Filter delegate;
    private final Condition condition;
    /**
     * Make {@link Filter} optional.
     *
     * @param delegate
     *            {@link Filter} which will be conditionally applied;
     * @param condition
     *            The {@link Condition} which have to be fulfilled in order to apply the filter.
     */
    public OptionalFilter(Filter delegate, Condition condition) {
        this.delegate = delegate;
        this.condition = condition;
    }
    @Override
    public Promise<Response, NeverThrowsException> filter(Context context, Request request, Handler next) {
        if (condition.canApplyFilter(context, request)) {
            return delegate.filter(context, request, next);
        }
        return next.handle(context, request);
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/ProxiedAuthV2Filter.java
New file
@@ -0,0 +1,223 @@
/*
 * 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 2016 ForgeRock AS.
 */
package org.forgerock.opendj.rest2ldap.authz;
import static org.forgerock.opendj.ldap.controls.ProxiedAuthV2RequestControl.newControl;
import static org.forgerock.opendj.rest2ldap.authz.Utils.asErrorResponse;
import static org.forgerock.opendj.rest2ldap.authz.Utils.close;
import static org.forgerock.services.context.SecurityContext.AUTHZID_DN;
import static org.forgerock.services.context.SecurityContext.AUTHZID_ID;
import static org.forgerock.util.Reject.checkNotNull;
import java.util.concurrent.atomic.AtomicReference;
import org.forgerock.http.Filter;
import org.forgerock.http.Handler;
import org.forgerock.http.protocol.Request;
import org.forgerock.http.protocol.Response;
import org.forgerock.opendj.ldap.Connection;
import org.forgerock.opendj.ldap.ConnectionFactory;
import org.forgerock.opendj.ldap.IntermediateResponseHandler;
import org.forgerock.opendj.ldap.LdapException;
import org.forgerock.opendj.ldap.LdapPromise;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.SearchResultHandler;
import org.forgerock.opendj.ldap.controls.Control;
import org.forgerock.opendj.ldap.controls.ProxiedAuthV2RequestControl;
import org.forgerock.opendj.ldap.requests.AddRequest;
import org.forgerock.opendj.ldap.requests.CompareRequest;
import org.forgerock.opendj.ldap.requests.DeleteRequest;
import org.forgerock.opendj.ldap.requests.ExtendedRequest;
import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
import org.forgerock.opendj.ldap.requests.ModifyRequest;
import org.forgerock.opendj.ldap.requests.SearchRequest;
import org.forgerock.opendj.ldap.responses.CompareResult;
import org.forgerock.opendj.ldap.responses.ExtendedResult;
import org.forgerock.opendj.ldap.responses.Result;
import org.forgerock.opendj.ldap.schema.Schema;
import org.forgerock.opendj.rest2ldap.AuthenticatedConnectionContext;
import org.forgerock.services.context.Context;
import org.forgerock.services.context.SecurityContext;
import org.forgerock.util.AsyncFunction;
import org.forgerock.util.Function;
import org.forgerock.util.promise.NeverThrowsException;
import org.forgerock.util.promise.Promise;
/**
 * Inject an {@link AuthenticatedConnectionContext} following the information provided in the {@link SecurityContext}.
 * This connection will add a {@link ProxiedAuthV2RequestControl} to each LDAP requests.
 */
public final class ProxiedAuthV2Filter implements Filter {
    private final ConnectionFactory connectionFactory;
    private final Function<SecurityContext, String, LdapException> authzProvider;
    /**
     * Create a new ProxyAuthzFilter. The {@link Connection} contained in the injected
     * {@link AuthenticatedConnectionContext} will use a {@link ProxiedAuthV2RequestControl} to perform authorization.
     * The authorizationID of this control is computed for each request by resolving the authzTemplate against the
     * {@link SecurityContext}.
     *
     * @param connectionFactory
     *            Factory used to get the {@link Connection}
     * @param authzIdProvider
     *            Function in charge of providing the authzid to use for the ProxiedAuth control
     * @throws NullPointerException
     *             If a parameter is null
     */
    public ProxiedAuthV2Filter(final ConnectionFactory connectionFactory,
            final Function<SecurityContext, String, LdapException> authzIdProvider) {
        this.connectionFactory = checkNotNull(connectionFactory, "connectionFactory cannot be null");
        this.authzProvider = checkNotNull(authzIdProvider, "authzIdProvider cannot be null");
    }
    @Override
    public Promise<Response, NeverThrowsException> filter(final Context context, final Request request,
            final Handler next) {
        final AtomicReference<Connection> connectionHolder = new AtomicReference<>();
        return connectionFactory
                .getConnectionAsync()
                .then(new Function<Connection, Connection, LdapException>() {
                    @Override
                    public Connection apply(Connection connection) throws LdapException {
                        connectionHolder.set(connection);
                        final Connection proxiedConnection = newProxiedConnection(
                                connection, authzProvider.apply(context.asContext(SecurityContext.class)));
                        connectionHolder.set(proxiedConnection);
                        return proxiedConnection;
                    }
                })
                .thenAsync(new AsyncFunction<Connection, Response, NeverThrowsException>() {
                    @Override
                    public Promise<Response, NeverThrowsException> apply(Connection connection) {
                        return next.handle(new AuthenticatedConnectionContext(context, connection), request);
                    }
                }, new AsyncFunction<LdapException, Response, NeverThrowsException>() {
                    @Override
                    public Promise<Response, NeverThrowsException> apply(LdapException ldapException) {
                        return asErrorResponse(ldapException);
                    }
                })
                .thenFinally(close(connectionHolder));
    }
    private Connection newProxiedConnection(Connection baseConnection, String authzId) {
        return new CachedReadConnectionDecorator(
                new ProxiedAuthConnectionDecorator(baseConnection, newControl(authzId)));
    }
    /**
     * Introspect the content of the {@link SecurityContext} and return the contained DN, or the user's ID if DN is not
     * present.
     */
    public static final class IntrospectionAuthzProvider implements Function<SecurityContext, String, LdapException> {
        /** Singleton instance. */
        public static final Function<SecurityContext, String, LdapException> INSTANCE =
                new IntrospectionAuthzProvider();
        private IntrospectionAuthzProvider() {
        }
        @Override
        public String apply(SecurityContext securityContext) throws LdapException {
            Object candidate;
            candidate = securityContext.getAuthorization().get(AUTHZID_DN);
            if (candidate != null) {
                return "dn:" + candidate;
            }
            candidate = securityContext.getAuthorization().get(AUTHZID_ID);
            if (candidate != null) {
                return "u:" + candidate;
            }
            throw LdapException.newLdapException(ResultCode.AUTH_METHOD_NOT_SUPPORTED);
        }
    }
    /** Use a {@link AuthzIdTemplate} to compute the authzId from the provided {@link SecurityContext}. */
    public static final class TemplateAuthzProvider implements Function<SecurityContext, String, LdapException> {
        private final AuthzIdTemplate template;
        private final Schema schema;
        TemplateAuthzProvider(AuthzIdTemplate template, Schema schema) {
            this.template = checkNotNull(template, "template cannot be null");
            this.schema = checkNotNull(schema, "schema cannot be null");
        }
        @Override
        public String apply(SecurityContext securityContext) throws LdapException {
            try {
                return template.formatAsAuthzId(securityContext.getAuthorization(), schema);
            } catch (IllegalArgumentException e) {
                throw LdapException.newLdapException(ResultCode.OPERATIONS_ERROR);
            }
        }
    }
    private static final class ProxiedAuthConnectionDecorator extends AbstractAsynchronousConnectionDecorator {
        private final Control proxiedAuthzControl;
        ProxiedAuthConnectionDecorator(Connection delegate, Control proxiedAuthzControl) {
            super(delegate);
            this.proxiedAuthzControl = proxiedAuthzControl;
        }
        @Override
        public LdapPromise<Result> addAsync(AddRequest request,
                IntermediateResponseHandler intermediateResponseHandler) {
            return delegate.addAsync(request.addControl(proxiedAuthzControl), intermediateResponseHandler);
        }
        @Override
        public LdapPromise<CompareResult> compareAsync(final CompareRequest request,
                final IntermediateResponseHandler intermediateResponseHandler) {
            return delegate.compareAsync(request.addControl(proxiedAuthzControl), intermediateResponseHandler);
        }
        @Override
        public LdapPromise<Result> deleteAsync(final DeleteRequest request,
                final IntermediateResponseHandler intermediateResponseHandler) {
            return delegate.deleteAsync(request.addControl(proxiedAuthzControl), intermediateResponseHandler);
        }
        @Override
        public <R extends ExtendedResult> LdapPromise<R> extendedRequestAsync(final ExtendedRequest<R> request,
                final IntermediateResponseHandler intermediateResponseHandler) {
            return delegate.extendedRequestAsync(request.addControl(proxiedAuthzControl),
                    intermediateResponseHandler);
        }
        @Override
        public LdapPromise<Result> modifyAsync(final ModifyRequest request,
                final IntermediateResponseHandler intermediateResponseHandler) {
            return delegate.modifyAsync(request.addControl(proxiedAuthzControl), intermediateResponseHandler);
        }
        @Override
        public LdapPromise<Result> modifyDNAsync(final ModifyDNRequest request,
                final IntermediateResponseHandler intermediateResponseHandler) {
            return delegate.modifyDNAsync(request.addControl(proxiedAuthzControl), intermediateResponseHandler);
        }
        @Override
        public LdapPromise<Result> searchAsync(final SearchRequest request,
                final IntermediateResponseHandler intermediateResponseHandler, final SearchResultHandler entryHandler) {
            return delegate.searchAsync(request.addControl(proxiedAuthzControl), intermediateResponseHandler,
                    entryHandler);
        }
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/SASLPlainStrategy.java
New file
@@ -0,0 +1,133 @@
/*
 * 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 2016 ForgeRock AS.
 */
package org.forgerock.opendj.rest2ldap.authz;
import static org.forgerock.opendj.ldap.requests.Requests.newPlainSASLBindRequest;
import static org.forgerock.services.context.SecurityContext.AUTHZID_DN;
import static org.forgerock.services.context.SecurityContext.AUTHZID_ID;
import static org.forgerock.util.Reject.checkNotNull;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import org.forgerock.i18n.LocalizedIllegalArgumentException;
import org.forgerock.opendj.ldap.Connection;
import org.forgerock.opendj.ldap.ConnectionFactory;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.DecodeException;
import org.forgerock.opendj.ldap.DecodeOptions;
import org.forgerock.opendj.ldap.LdapException;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.controls.AuthorizationIdentityRequestControl;
import org.forgerock.opendj.ldap.controls.AuthorizationIdentityResponseControl;
import org.forgerock.opendj.ldap.responses.BindResult;
import org.forgerock.opendj.ldap.schema.Schema;
import org.forgerock.services.context.Context;
import org.forgerock.services.context.SecurityContext;
import org.forgerock.util.AsyncFunction;
import org.forgerock.util.Function;
import org.forgerock.util.promise.Promise;
/** Bind using a computed DN from a template and the current request/context. */
public final class SASLPlainStrategy implements AuthenticationStrategy {
    private final ConnectionFactory connectionFactory;
    private final Function<String, String, LdapException> formatter;
    /**
     * Create a new SASLPlainStrategy.
     *
     * @param connectionFactory
     *            Factory used to get {@link Connection} receiving the sasl-bind requests
     * @param authcIdTemplate
     *            Authentication identity template containing a single %s which will be replaced by the authenticating
     *            user's name. (i.e: (u:%s)
     * @param schema
     *            Schema used to perform DN validation.
     * @throws NullPointerException
     *             If a parameter is null
     */
    public SASLPlainStrategy(final ConnectionFactory connectionFactory, final Schema schema,
            final String authcIdTemplate) {
        this.connectionFactory = checkNotNull(connectionFactory, "connectionFactory cannot be null");
        checkNotNull(schema, "schema cannot be null");
        checkNotNull(authcIdTemplate, "authcIdTemplate cannot be null");
        if (authcIdTemplate.startsWith("dn:")) {
            formatter = new Function<String, String, LdapException>() {
                @Override
                public String apply(String value) throws LdapException {
                    try {
                        return DN.format(authcIdTemplate, schema, value).toString();
                    } catch (LocalizedIllegalArgumentException e) {
                        throw LdapException.newLdapException(ResultCode.INVALID_DN_SYNTAX, e.getMessageObject(), e);
                    }
                }
            };
        } else {
            formatter = new Function<String, String, LdapException>() {
                @Override
                public String apply(String value) throws LdapException {
                    return String.format(authcIdTemplate, value);
                }
            };
        }
    }
    @Override
    public Promise<SecurityContext, LdapException> authenticate(final String username, final String password,
            final Context parentContext, final AtomicReference<Connection> authenticateConnectionHolder) {
        return connectionFactory
                .getConnectionAsync()
                .thenAsync(new AsyncFunction<Connection, SecurityContext, LdapException>() {
                    @Override
                    public Promise<SecurityContext, LdapException> apply(Connection connection) throws LdapException {
                        authenticateConnectionHolder.set(connection);
                        final String authcId = formatter.apply(username);
                        return doSASLPlainBind(connection, parentContext, username, authcId, password);
                    }
                });
    }
    private Promise<SecurityContext, LdapException> doSASLPlainBind(final Connection connection,
            final Context parentContext, final String authzId, final String authcId, final String password) {
        return connection
                .bindAsync(newPlainSASLBindRequest(authcId, password.toCharArray())
                            .addControl(AuthorizationIdentityRequestControl.newControl(true)))
                .then(new Function<BindResult, SecurityContext, LdapException>() {
                    @Override
                    public SecurityContext apply(BindResult result) throws LdapException {
                        final Map<String, Object> authz = new LinkedHashMap<>(2);
                        try {
                            final AuthorizationIdentityResponseControl control =
                                    result.getControl(AuthorizationIdentityResponseControl.DECODER,
                                                      new DecodeOptions());
                            if (control != null) {
                                final String authzDN = control.getAuthorizationID();
                                if (authzDN.startsWith("dn:")) {
                                    authz.put(AUTHZID_DN, authzDN.substring(3));
                                }
                            }
                        } catch (DecodeException e) {
                            // Just ignore
                        }
                        authz.put(AUTHZID_ID, authzId);
                        return new SecurityContext(parentContext, authcId, authz);
                    }
                });
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/SearchThenBindStrategy.java
New file
@@ -0,0 +1,101 @@
/*
 * 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 2016 ForgeRock AS.
 */
package org.forgerock.opendj.rest2ldap.authz;
import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest;
import static org.forgerock.opendj.rest2ldap.authz.SimpleBindStrategy.doSimpleBind;
import static org.forgerock.opendj.rest2ldap.authz.Utils.close;
import static org.forgerock.util.Reject.checkNotNull;
import java.util.concurrent.atomic.AtomicReference;
import org.forgerock.opendj.ldap.Connection;
import org.forgerock.opendj.ldap.ConnectionFactory;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.LdapException;
import org.forgerock.opendj.ldap.SearchScope;
import org.forgerock.opendj.ldap.responses.SearchResultEntry;
import org.forgerock.services.context.Context;
import org.forgerock.services.context.SecurityContext;
import org.forgerock.util.AsyncFunction;
import org.forgerock.util.promise.Promise;
/** Bind using the result of a search request computed from the current request/context. */
public final class SearchThenBindStrategy implements AuthenticationStrategy {
    private final ConnectionFactory searchConnectionFactory;
    private final ConnectionFactory bindConnectionFactory;
    private final DN baseDN;
    private final SearchScope searchScope;
    private final String filterTemplate;
    /**
     * Create a new SearchThenBindStrategy.
     *
     * @param searchConnectionFactory
     *            Factory to use to perform the search operation
     * @param bindConnectionFactory
     *            Factory to use to perform the bind operation
     * @param baseDN
     *            BaseDN of the search request
     * @param searchScope
     *            Scope of the search request
     * @param filterTemplate
     *            Filter of the search request (i.e: (&(uid=%s)(objectClass=inetOrgPerson)
     * @throws NullPointerException
     *             If a parameter is null
     */
    public SearchThenBindStrategy(ConnectionFactory searchConnectionFactory, ConnectionFactory bindConnectionFactory,
            DN baseDN, SearchScope searchScope, String filterTemplate) {
        this.searchConnectionFactory = checkNotNull(searchConnectionFactory, "searchConnectionFactory cannot be null");
        this.bindConnectionFactory = checkNotNull(bindConnectionFactory, "bindConnectionFactory cannot be null");
        this.baseDN = checkNotNull(baseDN, "baseDN cannot be null");
        this.searchScope = checkNotNull(searchScope, "searchScope cannot be null");
        this.filterTemplate = checkNotNull(filterTemplate, "filterTemplate cannot be null");
    }
    @Override
    public Promise<SecurityContext, LdapException> authenticate(final String username, final String password,
            final Context parentContext, final AtomicReference<Connection> authenticatedConnectionHolder) {
        final AtomicReference<Connection> searchConnectionHolder = new AtomicReference<>();
        return searchConnectionFactory
                .getConnectionAsync()
                // Search
                .thenAsync(new AsyncFunction<Connection, SearchResultEntry, LdapException>() {
                    @Override
                    public Promise<SearchResultEntry, LdapException> apply(final Connection connection)
                            throws LdapException {
                        searchConnectionHolder.set(connection);
                        return connection.searchSingleEntryAsync(
                                newSearchRequest(baseDN, searchScope, Filter.format(filterTemplate, username), "1.1"));
                    }
                })
                .thenFinally(close(searchConnectionHolder))
                // Bind
                .thenAsync(new AsyncFunction<SearchResultEntry, SecurityContext, LdapException>() {
                    @Override
                    public Promise<SecurityContext, LdapException> apply(final SearchResultEntry searchResult)
                            throws LdapException {
                        return bindConnectionFactory
                                .getConnectionAsync()
                                .thenAsync(
                                        doSimpleBind(authenticatedConnectionHolder, parentContext, username,
                                                searchResult.getName(), password));
                    }
                });
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/SimpleBindStrategy.java
New file
@@ -0,0 +1,95 @@
/*
 * 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 2016 ForgeRock AS.
 */
package org.forgerock.opendj.rest2ldap.authz;
import static org.forgerock.opendj.ldap.requests.Requests.newSimpleBindRequest;
import static org.forgerock.services.context.SecurityContext.AUTHZID_DN;
import static org.forgerock.services.context.SecurityContext.AUTHZID_ID;
import static org.forgerock.util.Reject.checkNotNull;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import org.forgerock.opendj.ldap.Connection;
import org.forgerock.opendj.ldap.ConnectionFactory;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.LdapException;
import org.forgerock.opendj.ldap.responses.BindResult;
import org.forgerock.opendj.ldap.schema.Schema;
import org.forgerock.services.context.Context;
import org.forgerock.services.context.SecurityContext;
import org.forgerock.util.AsyncFunction;
import org.forgerock.util.Function;
import org.forgerock.util.promise.Promise;
/** Bind using a computed DN from a template and the current request/context. */
public final class SimpleBindStrategy implements AuthenticationStrategy {
    private final ConnectionFactory connectionFactory;
    private final Schema schema;
    private final String bindDNTemplate;
    /**
     * Create a new SimpleBindStrategy.
     *
     * @param connectionFactory
     *            Factory used to get {@link Connection} receiving the bind requests
     * @param schema
     *            Schema used to validate DN
     * @param bindDNTemplate
     *            The template which will be replaced by the authenticating user (i.e:
     *            uid=%s,ou=People,dc=example,dc=com)
     * @throws NullPointerException
     *             If a parameter is null
     */
    public SimpleBindStrategy(ConnectionFactory connectionFactory, String bindDNTemplate, Schema schema) {
        this.connectionFactory = checkNotNull(connectionFactory, "connectionFactory cannot be null");
        this.bindDNTemplate = checkNotNull(bindDNTemplate, "bindDNTemplate cannot be null");
        this.schema = checkNotNull(schema, "schema cannot be null");
    }
    @Override
    public Promise<SecurityContext, LdapException> authenticate(final String username, final String password,
            final Context parentContext, final AtomicReference<Connection> authenticatedConnectionHolder) {
        return connectionFactory
                .getConnectionAsync()
                .thenAsync(doSimpleBind(authenticatedConnectionHolder, parentContext, username,
                           DN.format(bindDNTemplate, schema, username), password));
    }
    static AsyncFunction<Connection, SecurityContext, LdapException> doSimpleBind(
            final AtomicReference<Connection> connectionHolder, final Context parentContext, final String username,
            final DN bindDN, final String password) {
        return new AsyncFunction<Connection, SecurityContext, LdapException>() {
            @Override
            public Promise<SecurityContext, LdapException> apply(Connection connection) throws LdapException {
                connectionHolder.set(connection);
                return connection
                        .bindAsync(newSimpleBindRequest(bindDN.toString(), password.toCharArray()))
                        .then(new Function<BindResult, SecurityContext, LdapException>() {
                            @Override
                            public SecurityContext apply(BindResult result) throws LdapException {
                                final Map<String, Object> authzid = new LinkedHashMap<>(2);
                                authzid.put(AUTHZID_DN, bindDN.toString());
                                authzid.put(AUTHZID_ID, username);
                                return new SecurityContext(parentContext, username, authzid);
                            }
                        });
            }
        };
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/Utils.java
New file
@@ -0,0 +1,54 @@
/*
 * 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 2016 ForgeRock AS.
 */
package org.forgerock.opendj.rest2ldap.authz;
import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException;
import static org.forgerock.util.Utils.closeSilently;
import java.io.Closeable;
import java.util.concurrent.atomic.AtomicReference;
import org.forgerock.http.protocol.Response;
import org.forgerock.http.protocol.Status;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.util.promise.NeverThrowsException;
import org.forgerock.util.promise.Promise;
import org.forgerock.util.promise.Promises;
final class Utils {
    private Utils() { }
    static Runnable close(final AtomicReference<? extends Closeable> holder) {
        return new Runnable() {
            @Override
            public void run() {
                closeSilently(holder.get());
            }
        };
    }
    static Promise<Response, NeverThrowsException> asErrorResponse(final Throwable t) {
        final ResourceException e = asResourceException(t);
        final Response response = new Response()
                .setStatus(Status.valueOf(e.getCode()))
                .setEntity(e.toJsonValue().getObject());
        if (response.getStatus() == Status.UNAUTHORIZED) {
            response.getHeaders().put("WWW-Authenticate", "Basic");
        }
        return Promises.newResultPromise(response);
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/package-info.java
New file
@@ -0,0 +1,25 @@
/*
 * 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 2016 ForgeRock AS.
 */
/**
 * This package contains {@link org.forgerock.http.Filter} to authenticate and authorize LDAP connections. Authorization
 * filter injects a {@link org.forgerock.services.context.SecurityContext} populated with authorization information like
 * user's id, user's DN or anything else. This {@link org.forgerock.services.context.SecurityContext} can then be used
 * by {@link org.forgerock.opendj.rest2ldap.authz.ProxiedAuthV2Filter} to inject an
 * {@link org.forgerock.opendj.rest2ldap.AuthenticatedConnectionContext} containing the
 * {@link org.forgerock.opendj.ldap.Connection} with user specific privileges.
 */
package org.forgerock.opendj.rest2ldap.authz;
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/package-info.java
@@ -11,11 +11,16 @@
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2012 ForgeRock AS.
 * Copyright 2012-2016 ForgeRock AS.
 */
/**
 * APIs for implementing REST to LDAP gateways.
 * APIs for implementing REST to LDAP gateways. The API is implemented by
 * {@link org.forgerock.opendj.rest2ldap.LDAPCollectionResourceProvider} which is using a pre-established
 * {@link org.forgerock.opendj.ldap.Connection} encapsulated in the
 * {@link org.forgerock.opendj.rest2ldap.AuthenticatedConnectionContext}. This context is injected by the
 * {@link org.forgerock.opendj.rest2ldap.authz.ProxiedAuthV2Filter} depending on the
 * {@link org.forgerock.services.context.SecurityContext} injected by one of the configured authorization filters.
 */
package org.forgerock.opendj.rest2ldap;
opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java
@@ -11,7 +11,7 @@
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2013-2015 ForgeRock AS.
 * Copyright 2013-2016 ForgeRock AS.
 */
package org.forgerock.opendj.rest2ldap;
@@ -53,10 +53,10 @@
import org.forgerock.json.resource.NotSupportedException;
import org.forgerock.json.resource.PreconditionFailedException;
import org.forgerock.json.resource.QueryResponse;
import org.forgerock.json.resource.Requests;
import org.forgerock.json.resource.ResourceResponse;
import org.forgerock.opendj.ldap.ConnectionFactory;
import org.forgerock.opendj.ldap.IntermediateResponseHandler;
import org.forgerock.opendj.ldap.LdapException;
import org.forgerock.opendj.ldap.LdapResultHandler;
import org.forgerock.opendj.ldap.MemoryBackend;
import org.forgerock.opendj.ldap.RequestContext;
@@ -77,6 +77,7 @@
import org.forgerock.opendj.ldap.responses.Result;
import org.forgerock.opendj.ldif.LDIFEntryReader;
import org.forgerock.opendj.rest2ldap.Rest2LDAP.Builder;
import org.forgerock.services.context.Context;
import org.forgerock.testng.ForgeRockTestCase;
import org.forgerock.util.query.QueryFilter;
import org.testng.annotations.Test;
@@ -96,7 +97,7 @@
        final Connection connection = newConnection();
        final List<ResourceResponse> resources = new LinkedList<>();
        final QueryResponse result = connection.query(
            ctx(), Requests.newQueryRequest("").setQueryFilter(NO_FILTER), resources);
                newAuthConnectionContext(), newQueryRequest("").setQueryFilter(NO_FILTER), resources);
        assertThat(resources).hasSize(5);
        assertThat(result.getPagedResultsCookie()).isNull();
        assertThat(result.getTotalPagedResults()).isEqualTo(-1);
@@ -106,8 +107,8 @@
    public void testQueryNone() throws Exception {
        final Connection connection = newConnection();
        final List<ResourceResponse> resources = new LinkedList<>();
        final QueryResponse result = connection.query(
            ctx(), Requests.newQueryRequest("").setQueryFilter(QueryFilter.<JsonPointer>alwaysFalse()), resources);
        final QueryResponse result = connection.query(newAuthConnectionContext(),
                newQueryRequest("").setQueryFilter(QueryFilter.<JsonPointer> alwaysFalse()), resources);
        assertThat(resources).hasSize(0);
        assertThat(result.getPagedResultsCookie()).isNull();
        assertThat(result.getTotalPagedResults()).isEqualTo(-1);
@@ -120,7 +121,7 @@
        // Read first page.
        QueryResponse result = connection.query(
                ctx(), newQueryRequest("").setQueryFilter(NO_FILTER).setPageSize(2), resources);
                newAuthConnectionContext(), newQueryRequest("").setQueryFilter(NO_FILTER).setPageSize(2), resources);
        assertThat(result.getPagedResultsCookie()).isNotNull();
        assertThat(resources).hasSize(2);
        assertThat(resources.get(0).getId()).isEqualTo("test1");
@@ -130,7 +131,7 @@
        resources.clear();
        // Read second page.
        result = connection.query(ctx(),
        result = connection.query(newAuthConnectionContext(),
                newQueryRequest("").setQueryFilter(NO_FILTER).setPageSize(2).setPagedResultsCookie(cookie), resources);
        assertThat(result.getPagedResultsCookie()).isNotNull();
        assertThat(resources).hasSize(2);
@@ -141,7 +142,7 @@
        resources.clear();
        // Read third page.
        result = connection.query(ctx(),
        result = connection.query(newAuthConnectionContext(),
                newQueryRequest("").setQueryFilter(NO_FILTER).setPageSize(2).setPagedResultsCookie(cookie), resources);
        assertThat(result.getPagedResultsCookie()).isNull();
        assertThat(resources).hasSize(1);
@@ -152,7 +153,7 @@
    public void testQueryPageResultsIndexed() throws Exception {
        final Connection connection = newConnection();
        final List<ResourceResponse> resources = new ArrayList<>();
        QueryResponse result = connection.query(ctx(),
        QueryResponse result = connection.query(newAuthConnectionContext(),
                newQueryRequest("").setQueryFilter(NO_FILTER).setPageSize(2).setPagedResultsOffset(1), resources);
        assertThat(result.getPagedResultsCookie()).isNotNull();
        assertThat(resources).hasSize(2);
@@ -162,47 +163,53 @@
    @Test(expectedExceptions = NotFoundException.class)
    public void testDelete() throws Exception {
        final Context context = newAuthConnectionContext();
        final Connection connection = newConnection();
        final ResourceResponse resource = connection.delete(ctx(), newDeleteRequest("/test1"));
        final ResourceResponse resource = connection.delete(context, newDeleteRequest("/test1"));
        checkResourcesAreEqual(resource, getTestUser1(12345));
        connection.read(ctx(), newReadRequest("/test1"));
        connection.read(context, newReadRequest("/test1"));
    }
    @Test(expectedExceptions = NotFoundException.class)
    public void testDeleteMVCCMatch() throws Exception {
        final Context context = newAuthConnectionContext();
        final Connection connection = newConnection();
        final ResourceResponse resource = connection.delete(ctx(), newDeleteRequest("/test1").setRevision("12345"));
        final ResourceResponse resource = connection.delete(context, newDeleteRequest("/test1").setRevision("12345"));
        checkResourcesAreEqual(resource, getTestUser1(12345));
        connection.read(ctx(), newReadRequest("/test1"));
        connection.read(context, newReadRequest("/test1"));
    }
    @Test(expectedExceptions = PreconditionFailedException.class)
    public void testDeleteMVCCNoMatch() throws Exception {
        final Context context = newAuthConnectionContext();
        final Connection connection = newConnection();
        connection.delete(ctx(), newDeleteRequest("/test1").setRevision("12346"));
        connection.delete(context, newDeleteRequest("/test1").setRevision("12346"));
    }
    @Test(expectedExceptions = NotFoundException.class)
    public void testDeleteNotFound() throws Exception {
        final Context context = newAuthConnectionContext();
        final Connection connection = newConnection();
        connection.delete(ctx(), newDeleteRequest("/missing"));
        connection.delete(context, newDeleteRequest("/missing"));
    }
    @Test
    public void testPatch() throws Exception {
        final Context context = newAuthConnectionContext();
        final Connection connection = newConnection();
        final ResourceResponse resource1 =
                connection.patch(ctx(), newPatchRequest("/test1", add("/name/displayName", "changed")));
        final ResourceResponse resource1 = connection.patch(context,
                newPatchRequest("/test1", add("/name/displayName", "changed")));
        checkResourcesAreEqual(resource1, getTestUser1Updated(12345));
        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
        final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
        checkResourcesAreEqual(resource2, getTestUser1Updated(12345));
    }
    @Test
    public void testPatchEmpty() throws Exception {
        final List<Request> requests = new LinkedList<>();
        final Context context = newAuthConnectionContext(requests);
        final Connection connection = newConnection(requests);
        final ResourceResponse resource1 = connection.patch(ctx(), newPatchRequest("/test1"));
        final ResourceResponse resource1 = connection.patch(context, newPatchRequest("/test1"));
        checkResourcesAreEqual(resource1, getTestUser1(12345));
        /*
@@ -212,150 +219,162 @@
        assertThat(requests).hasSize(1);
        assertThat(requests.get(0)).isInstanceOf(SearchRequest.class);
        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
        final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
        checkResourcesAreEqual(resource2, getTestUser1(12345));
    }
    @Test
    public void testPatchAddOptionalAttribute() throws Exception {
        final Context context = newAuthConnectionContext();
        final Connection connection = newConnection();
        final JsonValue newContent = getTestUser1(12345);
        newContent.put("description", asList("one", "two"));
        final ResourceResponse resource1 =
                connection.patch(ctx(), newPatchRequest("/test1", add("/description", asList("one",
                        "two"))));
                connection.patch(context,
                                 newPatchRequest("/test1", add("/description", asList("one", "two"))));
        checkResourcesAreEqual(resource1, newContent);
        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
        final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
        checkResourcesAreEqual(resource2, newContent);
    }
    @Test
    public void testPatchAddOptionalAttributeIndexAppend() throws Exception {
        final Context context = newAuthConnectionContext();
        final Connection connection = newConnection();
        final JsonValue newContent = getTestUser1(12345);
        newContent.put("description", asList("one", "two"));
        final ResourceResponse resource1 = connection.patch(
            ctx(), newPatchRequest("/test1", add("/description/-", "one"), add("/description/-", "two")));
            context, newPatchRequest("/test1", add("/description/-", "one"), add("/description/-", "two")));
        checkResourcesAreEqual(resource1, newContent);
        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
        final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
        checkResourcesAreEqual(resource2, newContent);
    }
    @Test(expectedExceptions = BadRequestException.class)
    public void testPatchConstantAttribute() throws Exception {
        newConnection().patch(ctx(), newPatchRequest("/test1", add("/schemas", asList("junk"))));
        newConnection().patch(newAuthConnectionContext(), newPatchRequest("/test1", add("/schemas", asList("junk"))));
    }
    @Test
    public void testPatchDeleteOptionalAttribute() throws Exception {
        final Context context = newAuthConnectionContext();
        final Connection connection = newConnection();
        connection.patch(ctx(), newPatchRequest("/test1", add("/description", asList("one", "two"))));
        final ResourceResponse resource1 = connection.patch(ctx(), newPatchRequest("/test1", remove("/description")));
        connection.patch(context, newPatchRequest("/test1", add("/description", asList("one", "two"))));
        final ResourceResponse resource1 = connection.patch(context, newPatchRequest("/test1", remove("/description")));
        checkResourcesAreEqual(resource1, getTestUser1(12345));
        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
        final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
        checkResourcesAreEqual(resource2, getTestUser1(12345));
    }
    @Test
    public void testPatchIncrement() throws Exception {
        final Context context = newAuthConnectionContext();
        final Connection connection = newConnection();
        final JsonValue newContent = getTestUser1(12345);
        newContent.put("singleNumber", 100);
        newContent.put("multiNumber", asList(200, 300));
        final ResourceResponse resource1 = connection.patch(ctx(), newPatchRequest("/test1",
        final ResourceResponse resource1 = connection.patch(context, newPatchRequest("/test1",
            add("/singleNumber", 0),
            add("/multiNumber", asList(100, 200)),
            increment("/singleNumber", 100),
            increment("/multiNumber", 100)));
        checkResourcesAreEqual(resource1, newContent);
        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
        final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
        checkResourcesAreEqual(resource2, newContent);
    }
    @Test(expectedExceptions = BadRequestException.class)
    public void testPatchMissingRequiredAttribute() throws Exception {
        newConnection().patch(ctx(), newPatchRequest("/test1", remove("/name/surname")));
        newConnection().patch(newAuthConnectionContext(), newPatchRequest("/test1", remove("/name/surname")));
    }
    @Test
    public void testPatchModifyOptionalAttribute() throws Exception {
        final Connection connection = newConnection();
        connection.patch(ctx(), newPatchRequest("/test1", add("/description", asList("one", "two"))));
        final Context context = newAuthConnectionContext();
        connection.patch(context, newPatchRequest("/test1", add("/description", asList("one", "two"))));
        final ResourceResponse resource1 =
                connection.patch(ctx(), newPatchRequest("/test1", add("/description", asList("three"))));
                connection.patch(context, newPatchRequest("/test1", add("/description", asList("three"))));
        final JsonValue newContent = getTestUser1(12345);
        newContent.put("description", asList("one", "two", "three"));
        checkResourcesAreEqual(resource1, newContent);
        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
        final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
        checkResourcesAreEqual(resource2, newContent);
    }
    @Test(expectedExceptions = NotSupportedException.class)
    public void testPatchMultiValuedAttributeIndexAppend() throws Exception {
        final Connection connection = newConnection();
        connection.patch(ctx(), newPatchRequest("/test1", add("/description/0", "junk")));
        connection.patch(newAuthConnectionContext(), newPatchRequest("/test1", add("/description/0", "junk")));
    }
    @Test(expectedExceptions = BadRequestException.class)
    public void testPatchMultiValuedAttributeIndexAppendWithList() throws Exception {
        final Connection connection = newConnection();
        connection.patch(ctx(), newPatchRequest("/test1", add("/description/-",
        connection.patch(newAuthConnectionContext(), newPatchRequest("/test1", add("/description/-",
                asList("one", "two"))));
    }
    @Test(expectedExceptions = BadRequestException.class)
    public void testPatchMultiValuedAttributeWithSingleValue() throws Exception {
        final Connection connection = newConnection();
        connection.patch(ctx(), newPatchRequest("/test1", add("/description", "one")));
        connection.patch(newAuthConnectionContext(), newPatchRequest("/test1", add("/description", "one")));
    }
    @Test
    public void testPatchMVCCMatch() throws Exception {
        final Connection connection = newConnection();
        final Context context = newAuthConnectionContext();
        final ResourceResponse resource1 = connection.patch(
            ctx(), newPatchRequest("/test1", add("/name/displayName", "changed")).setRevision("12345"));
                context, newPatchRequest("/test1", add("/name/displayName", "changed")).setRevision("12345"));
        checkResourcesAreEqual(resource1, getTestUser1Updated(12345));
        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
        final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
        checkResourcesAreEqual(resource2, getTestUser1Updated(12345));
    }
    @Test(expectedExceptions = PreconditionFailedException.class)
    public void testPatchMVCCNoMatch() throws Exception {
        final Connection connection = newConnection();
        connection.patch(ctx(), newPatchRequest("/test1", add("/name/displayName", "changed")).setRevision("12346"));
        connection.patch(
                newAuthConnectionContext(),
                newPatchRequest("/test1", add("/name/displayName", "changed")).setRevision("12346"));
    }
    @Test(expectedExceptions = NotFoundException.class)
    public void testPatchNotFound() throws Exception {
        newConnection().patch(ctx(), newPatchRequest("/missing", add("/name/displayName", "changed")));
        newConnection().patch(
                newAuthConnectionContext(),
                newPatchRequest("/missing", add("/name/displayName", "changed")));
    }
    @Test(expectedExceptions = BadRequestException.class)
    public void testPatchReadOnlyAttribute() throws Exception {
        // Etag is read-only.
        newConnection().patch(ctx(), newPatchRequest("/test1", add("_rev", "99999")));
        newConnection().patch(newAuthConnectionContext(), newPatchRequest("/test1", add("_rev", "99999")));
    }
    @Test
    public void testPatchReplacePartialObject() throws Exception {
        final Connection connection = newConnection();
        final Context context = newAuthConnectionContext();
        final JsonValue expected = json(object(
            field("schemas", asList("urn:scim:schemas:core:1.0")),
            field("_id", "test1"),
            field("_rev", "12345"),
            field("name", object(field("displayName", "Humpty"),
                                 field("surname", "Dumpty")))));
        final ResourceResponse resource1 = connection.patch(ctx(), newPatchRequest("/test1",
        final ResourceResponse resource1 = connection.patch(context, newPatchRequest("/test1",
            replace("/name", object(field("displayName", "Humpty"), field("surname", "Dumpty")))));
        checkResourcesAreEqual(resource1, expected);
        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
        final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
        checkResourcesAreEqual(resource2, expected);
    }
    @Test
    public void testPatchReplaceWholeObject() throws Exception {
        final Connection connection = newConnection();
        final Context context = newAuthConnectionContext();
        final JsonValue newContent = json(object(
            field("name", object(field("displayName", "Humpty"),
                                 field("surname", "Dumpty")))));
@@ -366,70 +385,72 @@
            field("name", object(field("displayName", "Humpty"),
                                 field("surname", "Dumpty")))));
        final ResourceResponse resource1 =
                connection.patch(ctx(), newPatchRequest("/test1", replace("/", newContent)));
                connection.patch(context, newPatchRequest("/test1", replace("/", newContent)));
        checkResourcesAreEqual(resource1, expected);
        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
        final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
        checkResourcesAreEqual(resource2, expected);
    }
    @Test(expectedExceptions = BadRequestException.class)
    public void testPatchSingleValuedAttributeIndexAppend() throws Exception {
        final Connection connection = newConnection();
        connection.patch(ctx(), newPatchRequest("/test1", add("/name/surname/-", "junk")));
        connection.patch(newAuthConnectionContext(), newPatchRequest("/test1", add("/name/surname/-", "junk")));
    }
    @Test(expectedExceptions = NotSupportedException.class)
    public void testPatchSingleValuedAttributeIndexNumber() throws Exception {
        final Connection connection = newConnection();
        connection.patch(ctx(), newPatchRequest("/test1", add("/name/surname/0", "junk")));
        connection.patch(newAuthConnectionContext(), newPatchRequest("/test1", add("/name/surname/0", "junk")));
    }
    @Test(expectedExceptions = BadRequestException.class)
    public void testPatchSingleValuedAttributeWithMultipleValues() throws Exception {
        final Connection connection = newConnection();
        connection.patch(ctx(), newPatchRequest("/test1", add("/name/surname", asList("black",
        connection.patch(newAuthConnectionContext(), newPatchRequest("/test1", add("/name/surname", asList("black",
                "white"))));
    }
    @Test(expectedExceptions = BadRequestException.class)
    public void testPatchUnknownAttribute() throws Exception {
        final Connection connection = newConnection();
        connection.patch(ctx(), newPatchRequest("/test1", add("/dummy", "junk")));
        connection.patch(newAuthConnectionContext(), newPatchRequest("/test1", add("/dummy", "junk")));
    }
    @Test(expectedExceptions = BadRequestException.class)
    public void testPatchUnknownSubAttribute() throws Exception {
        final Connection connection = newConnection();
        connection.patch(ctx(), newPatchRequest("/test1", add("/description/dummy", "junk")));
        connection.patch(newAuthConnectionContext(), newPatchRequest("/test1", add("/description/dummy", "junk")));
    }
    @Test(expectedExceptions = BadRequestException.class)
    public void testPatchUnknownSubSubAttribute() throws Exception {
        final Connection connection = newConnection();
        connection.patch(ctx(), newPatchRequest("/test1", add("/description/dummy/dummy", "junk")));
        connection.patch(newAuthConnectionContext(),
                         newPatchRequest("/test1", add("/description/dummy/dummy", "junk")));
    }
    @Test
    public void testRead() throws Exception {
        final ResourceResponse resource = newConnection().read(ctx(), newReadRequest("/test1"));
        final ResourceResponse resource = newConnection().read(newAuthConnectionContext(), newReadRequest("/test1"));
        checkResourcesAreEqual(resource, getTestUser1(12345));
    }
    @Test(expectedExceptions = NotFoundException.class)
    public void testReadNotFound() throws Exception {
        newConnection().read(ctx(), newReadRequest("/missing"));
        newConnection().read(newAuthConnectionContext(), newReadRequest("/missing"));
    }
    @Test
    public void testReadSelectAllFields() throws Exception {
        final ResourceResponse resource = newConnection().read(ctx(), newReadRequest("/test1").addField("/"));
        final ResourceResponse resource = newConnection().read(newAuthConnectionContext(),
                                                               newReadRequest("/test1").addField("/"));
        checkResourcesAreEqual(resource, getTestUser1(12345));
    }
    @Test
    public void testReadSelectPartial() throws Exception {
        final ResourceResponse resource = newConnection().read(
            ctx(), newReadRequest("/test1").addField("/name/surname"));
            newAuthConnectionContext(), newReadRequest("/test1").addField("/name/surname"));
        assertThat(resource.getId()).isEqualTo("test1");
        assertThat(resource.getRevision()).isEqualTo("12345");
        assertThat(resource.getContent().get("_id").asString()).isNull();
@@ -442,7 +463,7 @@
    @Test(enabled = false)
    public void testReadSelectPartialInsensitive() throws Exception {
        final ResourceResponse resource = newConnection().read(
            ctx(), newReadRequest("/test1").addField("/name/SURNAME"));
            newAuthConnectionContext(), newReadRequest("/test1").addField("/name/SURNAME"));
        assertThat(resource.getId()).isEqualTo("test1");
        assertThat(resource.getRevision()).isEqualTo("12345");
        assertThat(resource.getContent().get("_id").asString()).isNull();
@@ -454,10 +475,11 @@
    @Test
    public void testUpdate() throws Exception {
        final Connection connection = newConnection();
        final Context context = newAuthConnectionContext();
        final ResourceResponse resource1 = connection.update(
            ctx(), newUpdateRequest("/test1", getTestUser1Updated(12345)));
                context, newUpdateRequest("/test1", getTestUser1Updated(12345)));
        checkResourcesAreEqual(resource1, getTestUser1Updated(12345));
        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
        final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
        checkResourcesAreEqual(resource2, getTestUser1Updated(12345));
    }
@@ -465,7 +487,8 @@
    public void testUpdateNoChange() throws Exception {
        final List<Request> requests = new LinkedList<>();
        final Connection connection = newConnection(requests);
        final ResourceResponse resource1 = connection.update(ctx(), newUpdateRequest("/test1", getTestUser1(12345)));
        final Context context = newAuthConnectionContext(requests);
        final ResourceResponse resource1 = connection.update(context, newUpdateRequest("/test1", getTestUser1(12345)));
        // Check that no modify operation was sent
        // (only a single search should be sent in order to get the current resource).
@@ -473,18 +496,19 @@
        assertThat(requests.get(0)).isInstanceOf(SearchRequest.class);
        checkResourcesAreEqual(resource1, getTestUser1(12345));
        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
        final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
        checkResourcesAreEqual(resource2, getTestUser1(12345));
    }
    @Test
    public void testUpdateAddOptionalAttribute() throws Exception {
        final Connection connection = newConnection();
        final Context context = newAuthConnectionContext();
        final JsonValue newContent = getTestUser1Updated(12345);
        newContent.put("description", asList("one", "two"));
        final ResourceResponse resource1 = connection.update(ctx(), newUpdateRequest("/test1", newContent));
        final ResourceResponse resource1 = connection.update(context, newUpdateRequest("/test1", newContent));
        checkResourcesAreEqual(resource1, newContent);
        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
        final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
        checkResourcesAreEqual(resource2, newContent);
    }
@@ -493,19 +517,20 @@
        final Connection connection = newConnection();
        final JsonValue newContent = getTestUser1Updated(12345);
        newContent.put("schemas", asList("junk"));
        connection.update(ctx(), newUpdateRequest("/test1", newContent));
        connection.update(newAuthConnectionContext(), newUpdateRequest("/test1", newContent));
    }
    @Test
    public void testUpdateDeleteOptionalAttribute() throws Exception {
        final Connection connection = newConnection();
        final Context context = newAuthConnectionContext();
        final JsonValue newContent = getTestUser1Updated(12345);
        newContent.put("description", asList("one", "two"));
        connection.update(ctx(), newUpdateRequest("/test1", newContent));
        connection.update(newAuthConnectionContext(), newUpdateRequest("/test1", newContent));
        newContent.remove("description");
        final ResourceResponse resource1 = connection.update(ctx(), newUpdateRequest("/test1", newContent));
        final ResourceResponse resource1 = connection.update(context, newUpdateRequest("/test1", newContent));
        checkResourcesAreEqual(resource1, newContent);
        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
        final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
        checkResourcesAreEqual(resource2, newContent);
    }
@@ -514,50 +539,52 @@
        final Connection connection = newConnection();
        final JsonValue newContent = getTestUser1Updated(12345);
        newContent.get("name").remove("surname");
        connection.update(ctx(), newUpdateRequest("/test1", newContent));
        connection.update(newAuthConnectionContext(), newUpdateRequest("/test1", newContent));
    }
    @Test
    public void testUpdateModifyOptionalAttribute() throws Exception {
        final Connection connection = newConnection();
        final Context context = newAuthConnectionContext();
        final JsonValue newContent = getTestUser1Updated(12345);
        newContent.put("description", asList("one", "two"));
        connection.update(ctx(), newUpdateRequest("/test1", newContent));
        connection.update(newAuthConnectionContext(), newUpdateRequest("/test1", newContent));
        newContent.put("description", asList("three"));
        final ResourceResponse resource1 = connection.update(ctx(), newUpdateRequest("/test1", newContent));
        final ResourceResponse resource1 = connection.update(context, newUpdateRequest("/test1", newContent));
        checkResourcesAreEqual(resource1, newContent);
        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
        final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
        checkResourcesAreEqual(resource2, newContent);
    }
    @Test
    public void testUpdateMVCCMatch() throws Exception {
        final Connection connection = newConnection();
        final Context context = newAuthConnectionContext();
        final ResourceResponse resource1 =
                connection.update(ctx(), newUpdateRequest("/test1", getTestUser1Updated(12345)).setRevision("12345"));
                connection.update(context, newUpdateRequest("/test1", getTestUser1Updated(12345)).setRevision("12345"));
        checkResourcesAreEqual(resource1, getTestUser1Updated(12345));
        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
        final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
        checkResourcesAreEqual(resource2, getTestUser1Updated(12345));
    }
    @Test(expectedExceptions = PreconditionFailedException.class)
    public void testUpdateMVCCNoMatch() throws Exception {
        final Connection connection = newConnection();
        connection.update(ctx(), newUpdateRequest("/test1", getTestUser1Updated(12345))
        connection.update(newAuthConnectionContext(), newUpdateRequest("/test1", getTestUser1Updated(12345))
                .setRevision("12346"));
    }
    @Test(expectedExceptions = NotFoundException.class)
    public void testUpdateNotFound() throws Exception {
        final Connection connection = newConnection();
        connection.update(ctx(), newUpdateRequest("/missing", getTestUser1Updated(12345)));
        connection.update(newAuthConnectionContext(), newUpdateRequest("/missing", getTestUser1Updated(12345)));
    }
    @Test(expectedExceptions = BadRequestException.class)
    public void testUpdateReadOnlyAttribute() throws Exception {
        final Connection connection = newConnection();
        // Etag is read-only.
        connection.update(ctx(), newUpdateRequest("/test1", getTestUser1Updated(99999)));
        connection.update(newAuthConnectionContext(), newUpdateRequest("/test1", getTestUser1Updated(99999)));
    }
    @Test(expectedExceptions = BadRequestException.class)
@@ -565,7 +592,7 @@
        final Connection connection = newConnection();
        final JsonValue newContent = getTestUser1Updated(12345);
        newContent.put("surname", asList("black", "white"));
        connection.update(ctx(), newUpdateRequest("/test1", newContent));
        connection.update(newAuthConnectionContext(), newUpdateRequest("/test1", newContent));
    }
    @Test(expectedExceptions = BadRequestException.class)
@@ -573,7 +600,7 @@
        final Connection connection = newConnection();
        final JsonValue newContent = getTestUser1Updated(12345);
        newContent.add("dummy", "junk");
        connection.update(ctx(), newUpdateRequest("/test1", newContent));
        connection.update(newAuthConnectionContext(), newUpdateRequest("/test1", newContent));
    }
    private Connection newConnection() throws IOException {
@@ -586,12 +613,10 @@
    private Builder builder(final List<Request> requests) throws IOException {
        return Rest2LDAP.builder()
                .ldapConnectionFactory(getConnectionFactory(requests))
                .baseDN("dc=test")
                .useEtagAttribute()
                .useClientDNNaming("uid")
                .readOnUpdatePolicy(ReadOnUpdatePolicy.CONTROLS)
                .authorizationPolicy(AuthorizationPolicy.NONE)
                .additionalLDAPAttribute("objectClass", "top", "person")
                .mapper(object()
                        .attribute("schemas", constant(asList("urn:scim:schemas:core:1.0")))
@@ -618,6 +643,15 @@
                expectedResource.getContent().getObject());
    }
    private AuthenticatedConnectionContext newAuthConnectionContext() throws LdapException, IOException {
        return newAuthConnectionContext(new ArrayList<Request>());
    }
    private AuthenticatedConnectionContext newAuthConnectionContext(List<Request> requests)
            throws LdapException, IOException {
        return new AuthenticatedConnectionContext(ctx(), getConnectionFactory(requests).getConnection());
    }
    private ConnectionFactory getConnectionFactory(final List<Request> requests) throws IOException {
        // @formatter:off
        final MemoryBackend backend =
opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/AuthzIdTemplateTest.java
File was renamed from opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/AuthzIdTemplateTest.java
@@ -13,14 +13,13 @@
 *
 * Copyright 2013-2016 ForgeRock AS.
 */
package org.forgerock.opendj.rest2ldap;
package org.forgerock.opendj.rest2ldap.authz;
import static org.fest.assertions.Assertions.assertThat;
import java.util.LinkedHashMap;
import java.util.Map;
import org.forgerock.json.resource.ForbiddenException;
import org.forgerock.opendj.ldap.schema.Schema;
import org.forgerock.testng.ForgeRockTestCase;
import org.testng.annotations.DataProvider;
@@ -101,7 +100,7 @@
    }
    @Test(dataProvider = "invalidTemplateData", expectedExceptions = ForbiddenException.class)
    @Test(dataProvider = "invalidTemplateData", expectedExceptions = IllegalArgumentException.class)
    public void testInvalidTemplateData(final String template, Map<String, Object> principals)
            throws Exception {
        new AuthzIdTemplate(template).formatAsAuthzId(principals, Schema.getDefaultSchema());
opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/DirectConnectionFilterTest.java
New file
@@ -0,0 +1,89 @@
/*
 * 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 2016 ForgeRock AS.
 */
package org.forgerock.opendj.rest2ldap.authz;
import static org.fest.assertions.Assertions.*;
import static org.forgerock.opendj.ldap.LdapException.*;
import static org.forgerock.opendj.ldap.ResultCode.*;
import static org.mockito.Matchers.*;
import static org.mockito.Mockito.*;
import java.util.concurrent.ExecutionException;
import org.forgerock.http.Handler;
import org.forgerock.http.protocol.Request;
import org.forgerock.http.protocol.Response;
import org.forgerock.http.protocol.Status;
import org.forgerock.opendj.ldap.Connection;
import org.forgerock.opendj.ldap.ConnectionFactory;
import org.forgerock.opendj.ldap.LdapException;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.rest2ldap.AuthenticatedConnectionContext;
import org.forgerock.services.context.Context;
import org.forgerock.services.context.RootContext;
import org.forgerock.testng.ForgeRockTestCase;
import org.forgerock.util.promise.Promise;
import org.forgerock.util.promise.Promises;
import org.mockito.ArgumentCaptor;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
@Test
public class DirectConnectionFilterTest extends ForgeRockTestCase {
    private ConnectionFactory connectionFactory;
    private Connection connection;
    private DirectConnectionFilter filter;
    @BeforeMethod
    public void setUp() {
        connectionFactory = mock(ConnectionFactory.class);
        connection = mock(Connection.class);
        filter = new DirectConnectionFilter(connectionFactory);
    }
    @Test
    public void testInjectAuthenticatedConnectionContext() {
        when(connectionFactory.getConnectionAsync()).thenReturn(newResultPromise(connection));
        final Handler handler = mock(Handler.class);
        final ArgumentCaptor<Context> captureContext = ArgumentCaptor.forClass(Context.class);
        when(handler.handle(captureContext.capture(), any(Request.class)))
                .thenReturn(Response.newResponsePromise(new Response()));
        filter.filter(new RootContext(), new Request(), handler);
        assertThat(captureContext.getValue().asContext(AuthenticatedConnectionContext.class).getConnection())
                .isSameAs(connection);
        verify(connection).close();
    }
    @Test
    public void testErrorResponseSentIfCannotGetConnection() throws InterruptedException, ExecutionException {
        when(connectionFactory.getConnectionAsync()).thenReturn(newExceptionPromise(ADMIN_LIMIT_EXCEEDED));
        final Response response = filter.filter(new RootContext(), new Request(), mock(Handler.class)).get();
        assertThat(response.getStatus()).isEqualTo(Status.PAYLOAD_TOO_LARGE);
    }
    private static Promise<Connection, LdapException> newResultPromise(Connection connection) {
        return Promises.<Connection, LdapException> newResultPromise(connection);
    }
    private static Promise<Connection, LdapException> newExceptionPromise(ResultCode result) {
        return Promises.<Connection, LdapException> newExceptionPromise(newLdapException(result));
    }
}
opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/HttpBasicAuthenticationFilterTest.java
New file
@@ -0,0 +1,121 @@
/*
 * 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 2016 ForgeRock AS.
 */
package org.forgerock.opendj.rest2ldap.authz;
import static org.fest.assertions.Assertions.*;
import static org.forgerock.opendj.rest2ldap.authz.HttpBasicAuthenticationFilter.HttpBasicExtractor.*;
import static org.mockito.Matchers.*;
import static org.mockito.Mockito.*;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicReference;
import org.assertj.core.api.SoftAssertions;
import org.forgerock.http.Handler;
import org.forgerock.http.protocol.Headers;
import org.forgerock.http.protocol.Request;
import org.forgerock.http.protocol.Response;
import org.forgerock.opendj.ldap.LdapException;
import org.forgerock.opendj.rest2ldap.authz.HttpBasicAuthenticationFilter.CustomHeaderExtractor;
import org.forgerock.opendj.rest2ldap.authz.HttpBasicAuthenticationFilter.HttpBasicExtractor;
import org.forgerock.services.context.Context;
import org.forgerock.services.context.SecurityContext;
import org.forgerock.testng.ForgeRockTestCase;
import org.forgerock.util.Pair;
import org.forgerock.util.encode.Base64;
import org.forgerock.util.promise.Promises;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
@Test
public class HttpBasicAuthenticationFilterTest extends ForgeRockTestCase {
    private static final String USERNAME = "Aladdin";
    private static final String PASSWORD = "open sesame";
    private static final String BASE64_USERPASS = Base64.encode((USERNAME + ":" + PASSWORD).getBytes());
    @DataProvider(name = "Invalid HTTP basic auth strings")
    public Object[][] getInvalidHttpBasicAuthStrings() {
        return new Object[][] { { null }, { "bla" }, { "basic " + Base64.encode("la:bli:blu".getBytes()) } };
    }
    @Test(dataProvider = "Invalid HTTP basic auth strings")
    public void parseUsernamePasswordFromInvalidAuthZHeader(String authZHeader) throws Exception {
        final AuthenticationStrategy strategy = mock(AuthenticationStrategy.class);
        final HttpBasicAuthenticationFilter filter =
                new HttpBasicAuthenticationFilter(strategy, HttpBasicExtractor.INSTANCE, false);
        final Request req = new Request();
        req.getHeaders().put(HTTP_BASIC_AUTH_HEADER, authZHeader);
        assertThat(filter.canApplyFilter(null, req)).isFalse();
    }
    @DataProvider(name = "Valid HTTP basic auth strings")
    public Object[][] getValidHttpBasicAuthStrings() {
        return new Object[][] { { "basic " + BASE64_USERPASS }, { "Basic " + BASE64_USERPASS } };
    }
    @Test(dataProvider = "Valid HTTP basic auth strings")
    public void parseUsernamePasswordFromValidAuthZHeader(String authZHeader) throws Exception {
        final Headers headers = new Headers();
        headers.put(HTTP_BASIC_AUTH_HEADER, authZHeader);
        assertThat(HttpBasicExtractor.INSTANCE.apply(headers)).isEqualTo(Pair.of(USERNAME, PASSWORD));
    }
    @Test
    public void sendUnauthorizedResponseWithHttpBasicAuthWillChallengeUserAgent() throws Exception {
        final AuthenticationStrategy failureStrategy = mock(AuthenticationStrategy.class);
        when(failureStrategy
                .authenticate(any(String.class), any(String.class), any(Context.class), any(AtomicReference.class)))
                .thenReturn(Promises.<SecurityContext, LdapException>newResultPromise(null));
        final HttpBasicAuthenticationFilter filter =
                new HttpBasicAuthenticationFilter(failureStrategy, HttpBasicExtractor.INSTANCE, false);
        final Response response = filter.filter(null, new Request(), mock(Handler.class)).get();
        verifyUnauthorizedOutputMessage(response);
    }
    private void verifyUnauthorizedOutputMessage(Response response) throws IOException {
        final SoftAssertions softly = new SoftAssertions();
        softly.assertThat(response.getStatus().getCode()).isEqualTo(401);
        softly.assertThat(response.getStatus().getReasonPhrase()).isEqualTo("Unauthorized");
        softly.assertThat(response.getEntity().getJson().toString())
                .isEqualTo("{code=401, reason=Unauthorized, message=Invalid Credentials}");
        softly.assertAll();
    }
    @Test
    public void extractUsernamePasswordHttpBasicAuthWillAcceptUserAgent() throws Exception {
        final Headers headers = new Headers();
        headers.add(HTTP_BASIC_AUTH_HEADER, "Basic " + BASE64_USERPASS);
        assertThat(HttpBasicExtractor.INSTANCE.apply(headers)).isEqualTo(Pair.of(USERNAME, PASSWORD));
    }
    @Test
    public void extractUsernamePasswordCustomHeaders() throws Exception {
        final String customHeaderUsername = "X-OpenIDM-Username";
        final String customHeaderPassword = "X-OpenIDM-Password";
        CustomHeaderExtractor cha = new CustomHeaderExtractor(customHeaderUsername, customHeaderPassword);
        Headers headers = new Headers();
        headers.add(customHeaderUsername, USERNAME);
        headers.add(customHeaderPassword, PASSWORD);
        assertThat(cha.apply(headers)).isEqualTo(Pair.of(USERNAME, PASSWORD));
    }
}
opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/OptionalFilterTest.java
New file
@@ -0,0 +1,65 @@
/*
 * 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 2016 ForgeRock AS.
 */
package org.forgerock.opendj.rest2ldap.authz;
import static org.mockito.Matchers.*;
import static org.mockito.Mockito.*;
import org.forgerock.http.Filter;
import org.forgerock.http.Handler;
import org.forgerock.http.protocol.Request;
import org.forgerock.opendj.rest2ldap.authz.OptionalFilter.Condition;
import org.forgerock.services.context.Context;
import org.forgerock.services.context.RootContext;
import org.forgerock.testng.ForgeRockTestCase;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
@Test
public class OptionalFilterTest extends ForgeRockTestCase {
    private OptionalFilter optionalFilter;
    private Filter filter;
    private Condition condition;
    @BeforeMethod
    public void setUp() {
        filter = mock(Filter.class);
        condition = mock(Condition.class);
        optionalFilter = new OptionalFilter(filter, condition);
    }
    @Test
    public void testFilterNotAppliedIfConditionIsFalse() {
        when(condition.canApplyFilter(any(Context.class), any(Request.class))).thenReturn(false);
        optionalFilter.filter(new RootContext(), new Request(), mock(Handler.class));
        verify(filter, never()).filter(any(RootContext.class), any(Request.class), any(Handler.class));
    }
    @Test
    public void testFilterAppliedIfConditionIsTrue() {
        when(condition.canApplyFilter(any(Context.class), any(Request.class))).thenReturn(true);
        final Context context = new RootContext();
        final Request request = new Request();
        final Handler handler = mock(Handler.class);
        optionalFilter.filter(context, request, handler);
        verify(filter).filter(same(context), same(request), same(handler));
    }
}
opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/ProxiedAuthV2FilterTest.java
New file
@@ -0,0 +1,202 @@
/*
 * 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 2016 ForgeRock AS.
 */
package org.forgerock.opendj.rest2ldap.authz;
import static org.fest.assertions.Assertions.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.HashMap;
import java.util.Map;
import org.forgerock.http.Handler;
import org.forgerock.http.protocol.Request;
import org.forgerock.http.protocol.Response;
import org.forgerock.opendj.ldap.AbstractSynchronousConnection;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.Connection;
import org.forgerock.opendj.ldap.ConnectionEventListener;
import org.forgerock.opendj.ldap.ConnectionFactory;
import org.forgerock.opendj.ldap.DecodeException;
import org.forgerock.opendj.ldap.DecodeOptions;
import org.forgerock.opendj.ldap.IntermediateResponseHandler;
import org.forgerock.opendj.ldap.LdapException;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.SearchResultHandler;
import org.forgerock.opendj.ldap.SearchScope;
import org.forgerock.opendj.ldap.controls.ProxiedAuthV2RequestControl;
import org.forgerock.opendj.ldap.requests.AddRequest;
import org.forgerock.opendj.ldap.requests.BindRequest;
import org.forgerock.opendj.ldap.requests.CompareRequest;
import org.forgerock.opendj.ldap.requests.DeleteRequest;
import org.forgerock.opendj.ldap.requests.ExtendedRequest;
import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
import org.forgerock.opendj.ldap.requests.ModifyRequest;
import org.forgerock.opendj.ldap.requests.Requests;
import org.forgerock.opendj.ldap.requests.SearchRequest;
import org.forgerock.opendj.ldap.requests.UnbindRequest;
import org.forgerock.opendj.ldap.responses.BindResult;
import org.forgerock.opendj.ldap.responses.CompareResult;
import org.forgerock.opendj.ldap.responses.ExtendedResult;
import org.forgerock.opendj.ldap.responses.Responses;
import org.forgerock.opendj.ldap.responses.Result;
import org.forgerock.opendj.rest2ldap.AuthenticatedConnectionContext;
import org.forgerock.services.context.Context;
import org.forgerock.services.context.RootContext;
import org.forgerock.services.context.SecurityContext;
import org.forgerock.testng.ForgeRockTestCase;
import org.forgerock.util.promise.Promises;
import org.mockito.ArgumentCaptor;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
@Test
public class ProxiedAuthV2FilterTest extends ForgeRockTestCase {
    private ProxiedAuthV2Filter filter;
    private ArgumentCaptor<Context> captureContext;
    private Handler handler;
    @BeforeMethod
    public void setUp() {
        captureContext = ArgumentCaptor.forClass(Context.class);
        handler = mock(Handler.class);
    }
    @Test
    public void testConnectionIsUsingProxiedAuthControlOnRequests() throws LdapException {
        final ConnectionFactory connectionFactory = mock(ConnectionFactory.class);
        when(connectionFactory.getConnectionAsync())
                .thenReturn(Promises.<Connection, LdapException> newResultPromise(new CheckConnection() {
                    @Override
                    void verifyRequest(org.forgerock.opendj.ldap.requests.Request request) throws LdapException {
                        assertThat(request.getControls()).isNotEmpty();
                        final ProxiedAuthV2RequestControl control;
                        try {
                            control = request.getControl(ProxiedAuthV2RequestControl.DECODER, new DecodeOptions());
                        } catch (DecodeException e) {
                            throw LdapException.newLdapException(ResultCode.OPERATIONS_ERROR);
                        }
                        assertThat(control.getValue())
                                .isEqualTo(ByteString.valueOfUtf8("dn:uid=whatever,ou=people,dc=com"));
                    }
                }));
        filter = new ProxiedAuthV2Filter(connectionFactory, ProxiedAuthV2Filter.IntrospectionAuthzProvider.INSTANCE);
        final Map<String, Object> authz = new HashMap<>();
        authz.put(SecurityContext.AUTHZID_DN, "uid=whatever,ou=people,dc=com");
        final SecurityContext securityContext = new SecurityContext(new RootContext(), "whatever", authz);
        when(handler.handle(captureContext.capture(), any(Request.class)))
                .thenReturn(Response.newResponsePromise(new Response()));
        filter.filter(securityContext, new Request(), handler);
        final Connection proxiedConnection = captureContext.getValue().asContext(AuthenticatedConnectionContext.class)
                .getConnection();
        proxiedConnection.add(Requests.newAddRequest("cn=test"));
        proxiedConnection.applyChange(Requests.newChangeRecord("dn: cn=test", "changetype: delete"));
        proxiedConnection.compare(Requests.newCompareRequest("cn=test", "foo", "bar"));
        proxiedConnection.search(Requests.newSearchRequest("cn=test", SearchScope.BASE_OBJECT, "(cn=test)", ""));
        proxiedConnection.modify(Requests.newModifyRequest("cn=test"));
        proxiedConnection.delete("cn=test");
        proxiedConnection.deleteSubtree("cn=test");
        proxiedConnection.extendedRequest("blah", ByteString.empty());
        proxiedConnection.modifyDN("cn=foo", "cn=bar");
    }
    private abstract static class CheckConnection extends AbstractSynchronousConnection {
        abstract void verifyRequest(org.forgerock.opendj.ldap.requests.Request request) throws LdapException;
        @Override
        public Result add(AddRequest request) throws LdapException {
            verifyRequest(request);
            return Responses.newResult(ResultCode.SUCCESS);
        }
        @Override
        public BindResult bind(BindRequest request) throws LdapException {
            return Responses.newBindResult(ResultCode.SUCCESS);
        }
        @Override
        public void close(UnbindRequest request, String reason) {
        }
        @Override
        public CompareResult compare(CompareRequest request) throws LdapException {
            verifyRequest(request);
            return Responses.newCompareResult(ResultCode.SUCCESS);
        }
        @Override
        public Result delete(DeleteRequest request) throws LdapException {
            verifyRequest(request);
            return Responses.newResult(ResultCode.SUCCESS);
        }
        @SuppressWarnings("unchecked")
        @Override
        public <R extends ExtendedResult> R extendedRequest(ExtendedRequest<R> request,
                IntermediateResponseHandler handler) throws LdapException {
            verifyRequest(request);
            return (R) Responses.newGenericExtendedResult(ResultCode.SUCCESS);
        }
        @Override
        public Result search(SearchRequest request, SearchResultHandler handler) throws LdapException {
            verifyRequest(request);
            return null;
        }
        @Override
        public Result modify(ModifyRequest request) throws LdapException {
            verifyRequest(request);
            return null;
        }
        @Override
        public Result modifyDN(ModifyDNRequest request) throws LdapException {
            verifyRequest(request);
            return null;
        }
        @Override
        public void addConnectionEventListener(ConnectionEventListener listener) {
        }
        @Override
        public boolean isClosed() {
            return false;
        }
        @Override
        public boolean isValid() {
            return false;
        }
        @Override
        public void removeConnectionEventListener(ConnectionEventListener listener) {
        }
        @Override
        public String toString() {
            return null;
        }
    }
}
opendj-server-legacy/resource/config/http-config.json
@@ -1,133 +1,174 @@
{
    // The Rest2LDAP authentication filter configuration. The filter will be
    // disabled if the configuration is not present. Upon successful
    // authentication the filter will create a security context containing the
    // following principals:
    //
    // "dn" - the DN of the user if known (may not be the case for sasl-plain)
    // "id" - the username used for authentication.
    "authenticationFilter" : {
        // Indicates whether the filter should allow HTTP BASIC authentication.
        "supportHTTPBasicAuthentication" : true,
   "authorization": {
      // The authorization policies to use. Supported policies are "anonymous", "basic" and "oauth2".
      "policies": [ "basic" ],
      // Perform all operations using a pre-authorization connection.
      "anonymous": {
         // Specify the connection factory to use to perform LDAP operations.
         // If missing, "root" factory will be used.
         "ldapConnectionFactory": "root",
         // Enable proxied authorization using the specified user DN.
         // If empty, anonymous proxied authorization will be used.
         // If missing, connection from the ldapConnectionFactory will be used as-is.
         "userDN": ""
      },
        // Indicates whether the filter should allow alternative authentication
        // and, if so, which HTTP headers it should obtain the username and
        // password from.
        "supportAltAuthentication"        : true,
        "altAuthenticationUsernameHeader" : "X-OpenIDM-Username",
        "altAuthenticationPasswordHeader" : "X-OpenIDM-Password",
       // Use HTTP Basic authentication's information to bind to the LDAP server.
       "basic": {
         // Indicates whether the filter should allow alternative authentication
           // and, if so, which HTTP headers it should obtain the username and
           // password from.
           "supportAltAuthentication"        : true,
           "altAuthenticationUsernameHeader" : "X-OpenIDM-Username",
           "altAuthenticationPasswordHeader" : "X-OpenIDM-Password",
            // Define which LDAP bind mechanism to use
            // Supported mechanisms are "simple", "sasl-plain", "search"
         "bind": "search",
         // Bind to the LDAP server using the DN built from the HTTP Basic's username
         "simple": {
            // Connection factory used to perform the bind operation.
            // If missing, "bind" factory will be used.
             "ldapConnectionFactory": "bind",
            // The Bind DN Template containing a single %s which will be replaced by the authenticating
                // user's name. (i.e: uid=%s,ou=People,dc=example,dc=com)
                // If missing, "%s" is used.
            "bindDNTemplate": "uid=%s,ou=People,dc=example,dc=com"
         },
         // Bind to the LDAP server using a SASL Plain request
         "sasl-plain": {
            // Connection factory used to perform the bind operation.
            // If missing, "bind" factory will be used.
             "ldapConnectionFactory": "bind",
            // Authentication identity template containing a single %s which will be replaced by the authenticating
                // user's name. (i.e: u:%s)
            "authcIdTemplate": "u:%s"
         },
         // Bind to the LDAP server using the resulting DN of a search request.
         "search": {
            // Connection factory used to perform the search operation.
            // If missing, "root" factory will be used.
            "searchLDAPConnectionFactory": "root",
            // Connection factory used to perform the bind operation.
            // If missing, "bind" factory will be used.
            "bindLDAPConnectionFactory": "bind",
                // The %s filter format parameters will be substituted with the client-provided username,
                // using LDAP filter string character escaping.
                "baseDN"         : "ou=people,dc=example,dc=com",
                "scope"          : "sub", // Or "one".
                "filterTemplate" : "(&(uid=%s)(objectClass=inetOrgPerson))"
         }
           // TODO: support for HTTP sessions?
       }
   },
        // The search parameters to use for "search-simple" authentication. The
        // %s filter format parameters will be substituted with the
        // client-provided username, using LDAP filter string character escaping.
        "searchBaseDN"         : "ou=people,dc=example,dc=com",
        "searchScope"          : "sub", // Or "one".
        "searchFilterTemplate" : "(&(uid=%s)(objectClass=inetOrgPerson))"
        // TODO: support for HTTP sessions?
    },
    // The Rest2LDAP Servlet configuration.
    "servlet" : {
        // The REST APIs and their LDAP attribute mappings.
        "mappings" : {
            "/users" : {
                "baseDN"              : "ou=people,dc=example,dc=com",
                "readOnUpdatePolicy"  : "controls",
                "useSubtreeDelete"    : false,
                "usePermissiveModify" : true,
                "etagAttribute"       : "etag",
                "namingStrategy"      : {
                    "strategy"    : "clientDNNaming",
                    "dnAttribute" : "uid"
                },
                "additionalLDAPAttributes" : [
                    {
                        "type" : "objectClass",
                        "values" : [
                            "top",
                            "person",
                            "organizationalPerson",
                            "inetOrgPerson"
                        ]
                    }
                ],
                "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",
                        "primaryKey"    : "uid",
                        "mapper"         : { "object" : {
                            "_id"         : { "simple"   : { "ldapAttribute" : "uid", "isSingleValued" : true, "isRequired" : true } },
                            "displayName" : { "simple"   : { "ldapAttribute" : "cn", "isSingleValued" : true, "writability" : "readOnlyDiscardWrites" } }
                        } }
                    } },
                    "groups"     : { "reference" : {
                        "ldapAttribute" : "isMemberOf",
                        "baseDN"        : "ou=groups,dc=example,dc=com",
                        "writability"   : "readOnly",
                        "primaryKey"    : "cn",
                        "mapper"        : { "object" : {
                            "_id"         : { "simple"   : { "ldapAttribute" : "cn", "isSingleValued" : true } }
                        } }
                    } },
                    "contactInformation" : { "object" : {
                        "telephoneNumber" : { "simple" : { "ldapAttribute" : "telephoneNumber", "isSingleValued" : true } },
                        "emailAddress"    : { "simple" : { "ldapAttribute" : "mail", "isSingleValued" : true } }
                    } },
                    "meta"        : { "object" : {
                        "created"      : { "simple" : { "ldapAttribute" : "createTimestamp", "isSingleValued" : true, "writability" : "readOnly" } },
                        "lastModified" : { "simple" : { "ldapAttribute" : "modifyTimestamp", "isSingleValued" : true, "writability" : "readOnly" } }
                    } }
                }
    // The REST APIs and their LDAP attribute mappings.
    "mappings" : {
        "/users" : {
            "baseDN"              : "ou=people,dc=example,dc=com",
            "readOnUpdatePolicy"  : "controls",
            "useSubtreeDelete"    : false,
            "usePermissiveModify" : true,
            "etagAttribute"       : "etag",
            "namingStrategy"      : {
                "strategy"    : "clientDNNaming",
                "dnAttribute" : "uid"
            },
            "/groups" : {
                "baseDN"              : "ou=groups,dc=example,dc=com",
                "readOnUpdatePolicy"  : "controls",
                "useSubtreeDelete"    : false,
                "usePermissiveModify" : true,
                "etagAttribute"       : "etag",
                "namingStrategy"      : {
                    "strategy"    : "clientDNNaming",
                    "dnAttribute" : "cn"
                },
                "additionalLDAPAttributes" : [
                    {
                        "type" : "objectClass",
                        "values" : [
                            "top",
                            "groupOfUniqueNames"
                        ]
                    }
                ],
                "attributes" : {
                    "schemas"     : { "constant" : [ "urn:scim:schemas:core:1.0" ] },
                    "_id"         : { "simple"   : { "ldapAttribute" : "cn", "isSingleValued" : true, "isRequired" : true, "writability" : "createOnly" } },
                    "_rev"        : { "simple"   : { "ldapAttribute" : "etag", "isSingleValued" : true, "writability" : "readOnly" } },
                    "displayName" : { "simple"   : { "ldapAttribute" : "cn", "isSingleValued" : true, "isRequired" : true, "writability" : "readOnly" } },
                    "members"    : { "reference" : {
                        "ldapAttribute" : "uniqueMember",
                        "baseDN"        : "dc=example,dc=com",
                        "primaryKey"    : "uid",
                        "mapper"        : { "object" : {
                            "_id"         : { "simple"   : { "ldapAttribute" : "uid", "isSingleValued" : true, "isRequired" : true } },
                            "displayName" : { "simple"   : { "ldapAttribute" : "cn", "isSingleValued" : true, "writability" : "readOnlyDiscardWrites" } }
                        } }
                    } },
                    "meta"        : { "object" : {
                        "created"      : { "simple" : { "ldapAttribute" : "createTimestamp", "isSingleValued" : true, "writability" : "readOnly" } },
                        "lastModified" : { "simple" : { "ldapAttribute" : "modifyTimestamp", "isSingleValued" : true, "writability" : "readOnly" } }
                    } }
            "additionalLDAPAttributes" : [
                {
                    "type" : "objectClass",
                    "values" : [
                        "top",
                        "person",
                        "organizationalPerson",
                        "inetOrgPerson"
                    ]
                }
            ],
            "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",
                    "primaryKey"    : "uid",
                    "mapper"         : { "object" : {
                        "_id"         : { "simple"   : { "ldapAttribute" : "uid", "isSingleValued" : true, "isRequired" : true } },
                        "displayName" : { "simple"   : { "ldapAttribute" : "cn", "isSingleValued" : true, "writability" : "readOnlyDiscardWrites" } }
                    } }
                } },
                "groups"     : { "reference" : {
                    "ldapAttribute" : "isMemberOf",
                    "baseDN"        : "ou=groups,dc=example,dc=com",
                    "writability"   : "readOnly",
                    "primaryKey"    : "cn",
                    "mapper"        : { "object" : {
                        "_id"         : { "simple"   : { "ldapAttribute" : "cn", "isSingleValued" : true } }
                    } }
                } },
                "contactInformation" : { "object" : {
                    "telephoneNumber" : { "simple" : { "ldapAttribute" : "telephoneNumber", "isSingleValued" : true } },
                    "emailAddress"    : { "simple" : { "ldapAttribute" : "mail", "isSingleValued" : true } }
                } },
                "meta"        : { "object" : {
                    "created"      : { "simple" : { "ldapAttribute" : "createTimestamp", "isSingleValued" : true, "writability" : "readOnly" } },
                    "lastModified" : { "simple" : { "ldapAttribute" : "modifyTimestamp", "isSingleValued" : true, "writability" : "readOnly" } }
                } }
            }
        },
        "/groups" : {
            "baseDN"              : "ou=groups,dc=example,dc=com",
            "readOnUpdatePolicy"  : "controls",
            "useSubtreeDelete"    : false,
            "usePermissiveModify" : true,
            "etagAttribute"       : "etag",
            "namingStrategy"      : {
                "strategy"    : "clientDNNaming",
                "dnAttribute" : "cn"
            },
            "additionalLDAPAttributes" : [
                {
                    "type" : "objectClass",
                    "values" : [
                        "top",
                        "groupOfUniqueNames"
                    ]
                }
            ],
            "attributes" : {
                "schemas"     : { "constant" : [ "urn:scim:schemas:core:1.0" ] },
                "_id"         : { "simple"   : { "ldapAttribute" : "cn", "isSingleValued" : true, "isRequired" : true, "writability" : "createOnly" } },
                "_rev"        : { "simple"   : { "ldapAttribute" : "etag", "isSingleValued" : true, "writability" : "readOnly" } },
                "displayName" : { "simple"   : { "ldapAttribute" : "cn", "isSingleValued" : true, "isRequired" : true, "writability" : "readOnly" } },
                "members"    : { "reference" : {
                    "ldapAttribute" : "uniqueMember",
                    "baseDN"        : "dc=example,dc=com",
                    "primaryKey"    : "uid",
                    "mapper"        : { "object" : {
                        "_id"         : { "simple"   : { "ldapAttribute" : "uid", "isSingleValued" : true, "isRequired" : true } },
                        "displayName" : { "simple"   : { "ldapAttribute" : "cn", "isSingleValued" : true, "writability" : "readOnlyDiscardWrites" } }
                    } }
                } },
                "meta"        : { "object" : {
                    "created"      : { "simple" : { "ldapAttribute" : "createTimestamp", "isSingleValued" : true, "writability" : "readOnly" } },
                    "lastModified" : { "simple" : { "ldapAttribute" : "modifyTimestamp", "isSingleValued" : true, "writability" : "readOnly" } }
                } }
            }
        }
    }
opendj-server-legacy/src/main/java/org/forgerock/opendj/adapter/server3x/Adapters.java
@@ -17,6 +17,8 @@
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.List;
import org.forgerock.opendj.ldap.AbstractSynchronousConnection;
import org.forgerock.opendj.ldap.ByteString;
@@ -51,17 +53,19 @@
import org.forgerock.opendj.ldap.responses.Responses;
import org.forgerock.opendj.ldap.responses.Result;
import org.forgerock.util.promise.Promise;
import org.forgerock.util.promise.PromiseImpl;
import org.opends.server.core.AddOperation;
import org.opends.server.core.BindOperation;
import org.opends.server.core.CompareOperation;
import org.opends.server.core.DeleteOperation;
import org.opends.server.core.DirectoryServer;
import org.opends.server.core.ExtendedOperation;
import org.opends.server.protocols.internal.InternalClientConnection;
import org.opends.server.protocols.internal.InternalSearchListener;
import org.opends.server.protocols.internal.InternalSearchOperation;
import org.opends.server.protocols.internal.Requests;
import org.opends.server.types.AuthenticationInfo;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.OperationType;
import org.opends.server.types.SearchFilter;
import org.opends.server.types.SearchResultEntry;
import org.opends.server.types.SearchResultReference;
@@ -69,7 +73,6 @@
import static org.forgerock.opendj.adapter.server3x.Converters.*;
import static org.forgerock.opendj.ldap.ByteString.*;
import static org.forgerock.opendj.ldap.LdapException.*;
import static org.forgerock.util.promise.Promises.*;
/** This class provides a connection factory and an adapter for the OpenDJ 2.x server. */
public final class Adapters {
@@ -90,33 +93,6 @@
    }
    /**
     * Returns a new anonymous connection factory.
     *
     * @return A new anonymous connection factory.
     */
    public static ConnectionFactory newAnonymousConnectionFactory() {
        InternalClientConnection icc = new InternalClientConnection(new AuthenticationInfo());
        return newConnectionFactory(icc);
    }
    /**
     * Returns a new connection factory for a specified user.
     *
     * @param userDN
     *            The specified user's DN.
     * @return a new connection factory.
     */
    public static ConnectionFactory newConnectionFactoryForUser(final DN userDN) {
        InternalClientConnection icc = null;
        try {
            icc = new InternalClientConnection(userDN);
        } catch (DirectoryException e) {
            throw new IllegalStateException(e.getMessage(), e);
        }
        return newConnectionFactory(icc);
    }
    /**
     * Returns a new connection factory.
     *
     * @param icc
@@ -125,7 +101,6 @@
     */
    public static ConnectionFactory newConnectionFactory(final InternalClientConnection icc) {
        return new ConnectionFactory() {
            @Override
            public void close() {
                // Nothing to do.
@@ -133,8 +108,30 @@
            @Override
            public Promise<Connection, LdapException> getConnectionAsync() {
                // TODO change the path...
                return newResultPromise(newConnection(icc));
                final PromiseImpl<Connection, LdapException> promise = PromiseImpl.create();
                try
                {
                  DirectoryServer.getWorkQueue().submitOperation(new AsyncOperation<>(icc, new Runnable()
                  {
                    @Override
                    public void run()
                    {
                      try
                      {
                        promise.handleResult(getConnection());
                      }
                      catch (LdapException e)
                      {
                        promise.handleException(e);
                      }
                    }
                  }));
                }
                catch (DirectoryException e)
                {
                  promise.handleException(LdapException.newLdapException(e.getResultCode()));
                }
                return promise;
            }
            @Override
@@ -144,214 +141,234 @@
        };
    }
    /**
     * Returns a new root connection.
     *
     * @return A new root connection.
     */
    public static Connection newRootConnection() {
        return newConnection(InternalClientConnection.getRootConnection());
    }
    /**
     * Returns a new connection for an anonymous user.
     *
     * @return A new connection.
     */
    public static Connection newAnonymousConnection() {
        return newConnection(new InternalClientConnection(new AuthenticationInfo()));
    }
    /**
     * Returns a new connection for a specified user.
     *
     * @param dn
     *            The DN of the user.
     * @return A new connection for a specified user.
     * @throws LdapException
     *             If no such object.
     */
    public static Connection newConnectionForUser(final DN dn) throws LdapException {
        try {
            return newConnection(new InternalClientConnection(dn));
        } catch (DirectoryException e) {
            throw newLdapException(Responses.newResult(ResultCode.NO_SUCH_OBJECT));
        }
    }
    private static Connection newConnection(final InternalClientConnection icc) {
        return new AbstractSynchronousConnection() {
      return new AbstractSynchronousConnection() {
            @Override
            public Result search(final SearchRequest request, final SearchResultHandler handler)
                    throws LdapException {
                InternalSearchListener internalSearchListener = new InternalSearchListener() {
          @Override
          public Result search(final SearchRequest request, final SearchResultHandler handler)
                  throws LdapException {
              InternalSearchListener internalSearchListener = new InternalSearchListener() {
                    @Override
                    public void handleInternalSearchReference(
                            InternalSearchOperation searchOperation,
                            SearchResultReference searchReference) throws DirectoryException {
                        handler.handleReference(from(searchReference));
                    }
                  @Override
                  public void handleInternalSearchReference(
                          InternalSearchOperation searchOperation,
                          SearchResultReference searchReference) throws DirectoryException {
                      handler.handleReference(from(searchReference));
                  }
                    @Override
                    public void handleInternalSearchEntry(InternalSearchOperation searchOperation,
                            SearchResultEntry searchEntry) throws DirectoryException {
                        handler.handleEntry(from(searchEntry));
                    }
                };
                  @Override
                  public void handleInternalSearchEntry(InternalSearchOperation searchOperation,
                          SearchResultEntry searchEntry) throws DirectoryException {
                      handler.handleEntry(from(searchEntry));
                  }
              };
                final SearchFilter filter = toSearchFilter(request.getFilter());
                final org.opends.server.protocols.internal.SearchRequest sr =
                    Requests.newSearchRequest(request.getName(), request.getScope(), filter)
                        .setDereferenceAliasesPolicy(request.getDereferenceAliasesPolicy())
                        .setSizeLimit(request.getSizeLimit())
                        .setTimeLimit(request.getTimeLimit())
                        .setTypesOnly(request.isTypesOnly())
                        .addAttribute(request.getAttributes())
                        .addControl(to(request.getControls()));
                return getResponseResult(icc.processSearch(sr, internalSearchListener));
            }
              final SearchFilter filter = toSearchFilter(request.getFilter());
              final org.opends.server.protocols.internal.SearchRequest sr =
                  Requests.newSearchRequest(request.getName(), request.getScope(), filter)
                      .setDereferenceAliasesPolicy(request.getDereferenceAliasesPolicy())
                      .setSizeLimit(request.getSizeLimit())
                      .setTimeLimit(request.getTimeLimit())
                      .setTypesOnly(request.isTypesOnly())
                      .addAttribute(request.getAttributes())
                      .addControl(to(request.getControls()));
              return getResponseResult(icc.processSearch(sr, internalSearchListener));
          }
            @Override
            public void removeConnectionEventListener(ConnectionEventListener listener) {
                // Internal client connection don't have any connection events.
            }
          @Override
          public void removeConnectionEventListener(ConnectionEventListener listener) {
              // Internal client connection don't have any connection events.
          }
            @Override
            public Result modifyDN(final ModifyDNRequest request) throws LdapException {
                return getResponseResult(icc.processModifyDN(request));
            }
          @Override
          public Result modifyDN(final ModifyDNRequest request) throws LdapException {
              return getResponseResult(icc.processModifyDN(request));
          }
            @Override
            public Result modify(final ModifyRequest request) throws LdapException {
                return getResponseResult(icc.processModify(request));
            }
          @Override
          public Result modify(final ModifyRequest request) throws LdapException {
              return getResponseResult(icc.processModify(request));
          }
            @Override
            public boolean isValid() {
                // Always true.
                return true;
            }
          @Override
          public boolean isValid() {
              // Always true.
              return true;
          }
            @Override
            public boolean isClosed() {
                return false;
            }
          @Override
          public boolean isClosed() {
              return false;
          }
            @Override
            public <R extends ExtendedResult> R extendedRequest(final ExtendedRequest<R> request,
                    final IntermediateResponseHandler handler) throws LdapException {
          @Override
          public <R extends ExtendedResult> R extendedRequest(final ExtendedRequest<R> request,
                  final IntermediateResponseHandler handler) throws LdapException {
                final ExtendedOperation extendedOperation =
                        icc.processExtendedOperation(request.getOID(), request.getValue(),
                                to(request.getControls()));
              final ExtendedOperation extendedOperation =
                      icc.processExtendedOperation(request.getOID(), request.getValue(),
                              to(request.getControls()));
                final Result result = getResponseResult(extendedOperation);
                final GenericExtendedResult genericExtendedResult =
                        Responses.newGenericExtendedResult(result.getResultCode())
                                .setDiagnosticMessage(result.getDiagnosticMessage()).setMatchedDN(
                                        result.getMatchedDN()).setValue(
                                        extendedOperation.getResponseValue().toByteString());
                try {
                    R extendedResult =
                            request.getResultDecoder().decodeExtendedResult(genericExtendedResult,
                                    new DecodeOptions());
                    for (final Control control : result.getControls()) {
                        extendedResult.addControl(control);
                    }
                    return extendedResult;
              final Result result = getResponseResult(extendedOperation);
              final GenericExtendedResult genericExtendedResult =
                      Responses.newGenericExtendedResult(result.getResultCode())
                              .setDiagnosticMessage(result.getDiagnosticMessage()).setMatchedDN(
                                      result.getMatchedDN()).setValue(
                                      extendedOperation.getResponseValue().toByteString());
              try {
                  R extendedResult =
                          request.getResultDecoder().decodeExtendedResult(genericExtendedResult,
                                  new DecodeOptions());
                  for (final Control control : result.getControls()) {
                      extendedResult.addControl(control);
                  }
                  return extendedResult;
                } catch (DecodeException e) {
                    DN matchedDN = extendedOperation.getMatchedDN();
                    return request.getResultDecoder().newExtendedErrorResult(
                            extendedOperation.getResultCode(),
                            matchedDN != null ? matchedDN.toString() : null,
                            extendedOperation.getErrorMessage().toString());
                }
            }
              } catch (DecodeException e) {
                  DN matchedDN = extendedOperation.getMatchedDN();
                  return request.getResultDecoder().newExtendedErrorResult(
                          extendedOperation.getResultCode(),
                          matchedDN != null ? matchedDN.toString() : null,
                          extendedOperation.getErrorMessage().toString());
              }
          }
            @Override
            public Result delete(final DeleteRequest request) throws LdapException {
                final DeleteOperation deleteOperation =
                        icc.processDelete(valueOfObject(request.getName()), to(request.getControls()));
                return getResponseResult(deleteOperation);
            }
          @Override
          public Result delete(final DeleteRequest request) throws LdapException {
              final DeleteOperation deleteOperation =
                      icc.processDelete(valueOfObject(request.getName()), to(request.getControls()));
              return getResponseResult(deleteOperation);
          }
            @Override
            public CompareResult compare(final CompareRequest request) throws LdapException {
                final CompareOperation compareOperation =
                        icc.processCompare(valueOfObject(request.getName()),
                                request.getAttributeDescription().toString(),
                                request.getAssertionValue(), to(request.getControls()));
          @Override
          public CompareResult compare(final CompareRequest request) throws LdapException {
              final CompareOperation compareOperation =
                      icc.processCompare(valueOfObject(request.getName()),
                              request.getAttributeDescription().toString(),
                              request.getAssertionValue(), to(request.getControls()));
                CompareResult result = Responses.newCompareResult(compareOperation.getResultCode());
                return getResponseResult(compareOperation, result);
            }
              CompareResult result = Responses.newCompareResult(compareOperation.getResultCode());
              return getResponseResult(compareOperation, result);
          }
            @Override
            public void close(final UnbindRequest request, final String reason) {
                // no implementation in open-ds.
            }
          @Override
          public void close(final UnbindRequest request, final String reason) {
              // no implementation in open-ds.
          }
            @Override
            public BindResult bind(final BindRequest request) throws LdapException {
                BindOperation bindOperation = null;
                if (request instanceof SimpleBindRequest) {
                    bindOperation =
                            icc.processSimpleBind(valueOfUtf8(request.getName()),
                                    ByteString.wrap(((SimpleBindRequest) request).getPassword()),
                                    to(request.getControls()));
                } else if (request instanceof SASLBindRequest) {
                    String serverName = null;
                    try {
                        serverName = InetAddress.getByName(null).getCanonicalHostName();
                    } catch (UnknownHostException e) {
                        // nothing to do.
                    }
                    BindClient bindClient = request.createBindClient(serverName);
                    do {
                        final GenericBindRequest genericBindRequest = bindClient.nextBindRequest();
                        bindOperation =
                                icc.processSASLBind(
                                        valueOfUtf8(request.getName()),
                                        ((SASLBindRequest) request).getSASLMechanism(),
                                        getCredentials(genericBindRequest.getAuthenticationValue()),
                                        to(request.getControls()));
                    } while (bindOperation.getResultCode() == ResultCode.SASL_BIND_IN_PROGRESS);
          @Override
          public BindResult bind(final BindRequest request) throws LdapException {
              BindOperation bindOperation = null;
              if (request instanceof SimpleBindRequest) {
                  bindOperation =
                          icc.processSimpleBind(valueOfUtf8(request.getName()),
                                  ByteString.wrap(((SimpleBindRequest) request).getPassword()),
                                  to(request.getControls()));
              } else if (request instanceof SASLBindRequest) {
                  String serverName = null;
                  try {
                      serverName = InetAddress.getByName(null).getCanonicalHostName();
                  } catch (UnknownHostException e) {
                      // nothing to do.
                  }
                  BindClient bindClient = request.createBindClient(serverName);
                  do {
                      final GenericBindRequest genericBindRequest = bindClient.nextBindRequest();
                      bindOperation =
                              icc.processSASLBind(
                                      valueOfUtf8(request.getName()),
                                      ((SASLBindRequest) request).getSASLMechanism(),
                                      getCredentials(genericBindRequest.getAuthenticationValue()),
                                      to(request.getControls()));
                  } while (bindOperation.getResultCode() == ResultCode.SASL_BIND_IN_PROGRESS);
                    bindClient.dispose();
                  bindClient.dispose();
                } else { // not supported
                    throw newLdapException(Responses.newResult(ResultCode.AUTH_METHOD_NOT_SUPPORTED));
                }
                BindResult result = Responses.newBindResult(bindOperation.getResultCode());
                result.setServerSASLCredentials(bindOperation.getSASLCredentials());
              } else { // not supported
                  throw newLdapException(Responses.newResult(ResultCode.AUTH_METHOD_NOT_SUPPORTED));
              }
              BindResult result = Responses.newBindResult(bindOperation.getResultCode());
              result.setServerSASLCredentials(bindOperation.getSASLCredentials());
                if (result.isSuccess()) {
                    return result;
                } else {
                    throw newLdapException(result);
                }
            }
              if (result.isSuccess()) {
                  return result;
              } else {
                  throw newLdapException(result);
              }
          }
            @Override
            public void addConnectionEventListener(ConnectionEventListener listener) {
                // Internal client connection don't have any connection events.
            }
          @Override
          public void addConnectionEventListener(ConnectionEventListener listener) {
              // Internal client connection don't have any connection events.
          }
            @Override
            public Result add(final AddRequest request) throws LdapException {
                final AddOperation addOperation =
                        icc.processAdd(valueOfObject(request.getName()), to(request
                                .getAllAttributes()), to(request.getControls()));
                return getResponseResult(addOperation);
            }
          @Override
          public Result add(final AddRequest request) throws LdapException {
              final AddOperation addOperation =
                      icc.processAdd(valueOfObject(request.getName()), to(request
                              .getAllAttributes()), to(request.getControls()));
              return getResponseResult(addOperation);
          }
            @Override
            public String toString() {
                return icc.toString();
            }
        };
          @Override
          public String toString() {
              return icc.toString();
          }
      };
  }
  /**
   * This operation is hack to be able to execute a {@link Runnable} in a
   * Directory Server's worker thread.
   */
  private static final class AsyncOperation<V> extends org.opends.server.types.AbstractOperation
  {
    private final Runnable runnable;
    AsyncOperation(InternalClientConnection icc, Runnable runnable)
    {
      super(icc, icc.nextOperationID(), icc.nextMessageID(), Collections.<org.opends.server.types.Control> emptyList());
      this.setInternalOperation(true);
      this.runnable = runnable;
    }
    @Override
    public void run()
    {
      runnable.run();
    }
    @Override
    public OperationType getOperationType()
    {
      return null;
    }
    @Override
    public List<org.opends.server.types.Control> getResponseControls()
    {
      return Collections.emptyList();
    }
    @Override
    public void addResponseControl(org.opends.server.types.Control control)
    {
    }
    @Override
    public void removeResponseControl(org.opends.server.types.Control control)
    {
    }
    @Override
    public DN getProxiedAuthorizationDN()
    {
      return null;
    }
    @Override
    public void setProxiedAuthorizationDN(DN proxiedAuthorizationDN)
    {
    }
    @Override
    public void toString(StringBuilder buffer)
    {
      buffer.append(AsyncOperation.class.getSimpleName());
    }
  }
}
opendj-server-legacy/src/main/java/org/opends/server/api/HttpEndpoint.java
@@ -24,6 +24,7 @@
import org.forgerock.http.HttpApplicationException;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.server.config.server.HTTPEndpointCfg;
import org.opends.server.core.ServerContext;
import org.opends.server.types.InitializationException;
/**
@@ -37,15 +38,21 @@
  /** Configuration of this endpoint. */
  protected final C configuration;
  /** Context of this LDAP server. */
  protected final ServerContext serverContext;
  /**
   * Create a new {@link HttpEndpoint} with the given configuration.
   *
   * @param configuration
   *          Configuration of this {@link HttpEndpoint}.
   *          Configuration of this {@link HttpEndpoint}
   * @param serverContext
   *          Context of this LDAP server
   */
  public HttpEndpoint(C configuration)
  public HttpEndpoint(C configuration, ServerContext serverContext)
  {
    this.configuration = configuration;
    this.serverContext = serverContext;
  }
  /**
opendj-server-legacy/src/main/java/org/opends/server/core/DirectoryServer.java
@@ -295,6 +295,9 @@
  /** The configuration handler for the Directory Server. */
  private ConfigurationHandler configurationHandler;
  /** The configuration manager that will handle HTTP endpoints. */
  private HttpEndpointConfigManager httpEndpointConfigManager;
  /** The set of account status notification handlers defined in the server. */
  private ConcurrentMap<DN, AccountStatusNotificationHandler<?>>
               accountStatusNotificationHandlers;
@@ -1554,9 +1557,6 @@
      identityMapperConfigManager = new IdentityMapperConfigManager(serverContext);
      identityMapperConfigManager.initializeIdentityMappers();
      new HttpEndpointConfigManager(httpRouter)
        .registerTo(serverContext.getServerManagementContext().getRootConfiguration());
      initializeRootDNConfigManager();
      initializeAuthenticatedUsers();
@@ -1645,6 +1645,9 @@
        admDataSync.synchronize();
      }
      httpEndpointConfigManager = new HttpEndpointConfigManager(serverContext);
      httpEndpointConfigManager.registerTo(serverContext.getServerManagementContext().getRootConfiguration());
      deleteUnnecessaryFiles();
    }
  }
opendj-server-legacy/src/main/java/org/opends/server/core/HttpEndpointConfigManager.java
@@ -49,22 +49,25 @@
 * removals, or modifications to any HTTP endpoints while the server is running.
 */
public class HttpEndpointConfigManager implements ConfigurationChangeListener<HTTPEndpointCfg>,
    ConfigurationAddListener<HTTPEndpointCfg>, ConfigurationDeleteListener<HTTPEndpointCfg>
                                                  ConfigurationAddListener<HTTPEndpointCfg>,
                                                  ConfigurationDeleteListener<HTTPEndpointCfg>
{
  private static final LocalizedLogger LOGGER = LocalizedLogger.getLoggerForThisClass();
  private final ServerContext serverContext;
  private final Router router;
  private final Map<DN, HttpApplication> applications;
  /**
   * Creates a new instance of this HTTP endpoint config manager.
   *
   * @param router
   *          The {@link Router} where to register configured {@link HttpEndpoint}
   * @param serverContext
   *          The server context.
   */
  public HttpEndpointConfigManager(Router router)
  public HttpEndpointConfigManager(ServerContext serverContext)
  {
    this.router = router;
    this.serverContext = serverContext;
    this.router = serverContext.getHTTPRouter();
    this.applications = new HashMap<>();
  }
@@ -197,7 +200,8 @@
      final Class<? extends HttpEndpoint<?>> endpointClass =
          (Class<? extends HttpEndpoint<?>>) HTTPEndpointCfgDefn.getInstance().getJavaClassPropertyDefinition()
              .loadClass(configuration.getJavaClass(), HttpEndpoint.class);
      return endpointClass.getDeclaredConstructor(configuration.configurationClass()).newInstance(configuration);
      return endpointClass.getDeclaredConstructor(configuration.configurationClass(), ServerContext.class)
                          .newInstance(configuration, serverContext);
    }
    catch (Exception e)
    {
opendj-server-legacy/src/main/java/org/opends/server/protocols/http/AuthenticationFilter.java
File was deleted
opendj-server-legacy/src/main/java/org/opends/server/protocols/http/HTTPAuthenticationConfig.java
File was deleted
opendj-server-legacy/src/main/java/org/opends/server/protocols/http/LDAPContext.java
@@ -13,30 +13,64 @@
 *
 * Copyright 2016 ForgeRock AS.
 */
package org.opends.server.protocols.http;
import org.forgerock.opendj.ldap.ConnectionFactory;
import org.forgerock.opendj.ldap.Connection;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.LdapException;
import org.forgerock.services.context.AbstractContext;
import org.forgerock.services.context.Context;
/** Context provided by this LDAP server to the embedded {@link org.forgerock.http.HttpApplication}s. */
/**
 * Context provided by a Directory Server. It contains a reference to a
 * {@link InternalConnectionFactory} which can be used to perform direct LDAP
 * operation on this Directory Server without the network overhead.
 */
public final class LDAPContext extends AbstractContext
{
  private final ConnectionFactory ldapConnectionFactory;
  private final InternalConnectionFactory internalConnectionFactory;
  LDAPContext(final Context parent, ConnectionFactory ldapConnectionFactory)
  /**
   * Create a new LDAPContext.
   *
   * @param parent
   *          The parent context.
   * @param internalConnectionFactory
   *          Internal connection factory of this LDAP server.
   */
  public LDAPContext(final Context parent, InternalConnectionFactory internalConnectionFactory)
  {
    super(parent, "LDAP context");
    this.ldapConnectionFactory = ldapConnectionFactory;
    this.internalConnectionFactory = internalConnectionFactory;
  }
  /**
   * Get the {@link org.forgerock.opendj.ldap.LDAPConnectionFactory} attached to this context.
   * Get the {@link InternalConnectionFactory} attached to this context.
   *
   * @return The {@link org.forgerock.opendj.ldap.LDAPConnectionFactory} attached to this context.
   * @return The {@link InternalConnectionFactory} attached to this context.
   */
  public ConnectionFactory getLdapConnectionFactory()
  public InternalConnectionFactory getInternalConnectionFactory()
  {
    return ldapConnectionFactory;
    return internalConnectionFactory;
  }
}
  /**
   * An internal connection factory providing direct connection to this Directory
   * Server without the network overhead.
   */
  public interface InternalConnectionFactory
  {
    /**
     * Get a direct {@link Connection} to this Directory Server.
     *
     * @param userDN
     *          DN of the user's used to validate authorization.
     * @return A direct {@link Connection} to this Directory Server.
     * @throws LdapException
     *           If a connection cannot be create (i.e: because the userDN
     *           doesn't exists).
     */
    Connection getConnection(DN userDN) throws LdapException;
  }
}
opendj-server-legacy/src/main/java/org/opends/server/protocols/http/LDAPContextInjectionFilter.java
@@ -15,22 +15,23 @@
 */
package org.opends.server.protocols.http;
import static org.opends.messages.ProtocolMessages.*;
import org.forgerock.http.Filter;
import org.forgerock.http.Handler;
import org.forgerock.http.protocol.Request;
import org.forgerock.http.protocol.Response;
import org.forgerock.http.protocol.Status;
import org.forgerock.opendj.ldap.Connection;
import org.forgerock.opendj.ldap.ConnectionFactory;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.LdapException;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.services.context.Context;
import org.forgerock.util.promise.NeverThrowsException;
import org.forgerock.util.promise.Promise;
import org.forgerock.util.promise.Promises;
import org.opends.server.core.DirectoryServer;
import org.opends.server.core.ServerContext;
import org.opends.server.types.DisconnectReason;
import org.opends.server.protocols.http.LDAPContext.InternalConnectionFactory;
import org.opends.server.types.AuthenticationInfo;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.Entry;
/**
 * Filter injecting the {@link LDAPContext} giving access to
@@ -41,45 +42,59 @@
  private final ServerContext serverContext;
  private final HTTPConnectionHandler httpConnectionHandler;
  LDAPContextInjectionFilter(ServerContext serverContext, HTTPConnectionHandler httpConnectionHandler) {
  LDAPContextInjectionFilter(ServerContext serverContext, HTTPConnectionHandler httpConnectionHandler)
  {
    this.serverContext = serverContext;
    this.httpConnectionHandler= httpConnectionHandler;
    this.httpConnectionHandler = httpConnectionHandler;
  }
  @Override
  public Promise<Response, NeverThrowsException> filter(Context context, Request request, Handler next)
  public Promise<Response, NeverThrowsException> filter(final Context context, final Request request,
      final Handler next)
  {
    final HTTPClientConnection clientConnection =
        new HTTPClientConnection(serverContext, httpConnectionHandler, context, request);
    if (clientConnection.getConnectionID() < 0)
    final LDAPContext djContext = new LDAPContext(context, new InternalConnectionFactory()
    {
      clientConnection.disconnect(DisconnectReason.ADMIN_LIMIT_EXCEEDED, true,
          ERR_CONNHANDLER_REJECTED_BY_SERVER.get());
      return Promises.newResultPromise(new Response(Status.SERVICE_UNAVAILABLE));
    }
    final LDAPContext djContext  = new LDAPContext(context, new ConnectionFactory()
    {
      private final Connection connection = new SdkConnectionAdapter(clientConnection);
      @Override
      public Promise<Connection, LdapException> getConnectionAsync()
      public Connection getConnection(DN userDN) throws LdapException
      {
        return Promises.newResultPromise(connection);
        final HTTPClientConnection clientConnection =
            new HTTPClientConnection(serverContext, httpConnectionHandler, context, request);
        clientConnection.setAuthenticationInfo(getAuthInfoForDN(userDN));
        if (clientConnection.getConnectionID() < 0)
        {
          throw LdapException.newLdapException(ResultCode.ADMIN_LIMIT_EXCEEDED);
        }
        httpConnectionHandler.addClientConnection(clientConnection);
        return new SdkConnectionAdapter(clientConnection);
      }
      @Override
      public Connection getConnection() throws LdapException
      private AuthenticationInfo getAuthInfoForDN(DN userDN) throws LdapException
      {
        return connection;
      }
      @Override
      public void close()
      {
        if (userDN == null || userDN.isRootDN())
        {
          return new AuthenticationInfo();
        }
        final DN rootUserDN = DirectoryServer.getActualRootBindDN(userDN);
        if (rootUserDN != null)
        {
          userDN = rootUserDN;
        }
        Entry userEntry;
        try
        {
          userEntry = DirectoryServer.getEntry(userDN);
        }
        catch (DirectoryException e)
        {
          throw LdapException.newLdapException(e.getResultCode());
        }
        if (userEntry == null)
        {
          throw LdapException.newLdapException(ResultCode.INVALID_CREDENTIALS);
        }
        return new AuthenticationInfo(userEntry, DirectoryServer.isRootDN(userDN));
      }
    });
    return next.handle(djContext, request);
  }
}
}
opendj-server-legacy/src/main/java/org/opends/server/protocols/http/rest2ldap/InternalProxyAuthzFilter.java
New file
@@ -0,0 +1,126 @@
/*
 * 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 2016 ForgeRock AS.
 */
package org.opends.server.protocols.http.rest2ldap;
import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException;
import static org.forgerock.util.Reject.checkNotNull;
import static org.forgerock.util.Utils.closeSilently;
import org.forgerock.http.Filter;
import org.forgerock.http.Handler;
import org.forgerock.http.protocol.Request;
import org.forgerock.http.protocol.Response;
import org.forgerock.http.protocol.Status;
import org.forgerock.i18n.LocalizedIllegalArgumentException;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.opendj.ldap.Connection;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.LdapException;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.schema.Schema;
import org.forgerock.opendj.rest2ldap.AuthenticatedConnectionContext;
import org.forgerock.services.context.Context;
import org.forgerock.services.context.SecurityContext;
import org.forgerock.util.Function;
import org.forgerock.util.promise.NeverThrowsException;
import org.forgerock.util.promise.Promise;
import org.forgerock.util.promise.Promises;
import org.opends.server.api.IdentityMapper;
import org.opends.server.protocols.http.LDAPContext;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.Entry;
/**
 * Authorization proxy using internal connection's capabilities as an optimized
 * alternative to {@link ProxiedAuthV2Control}. This proxy creates an
 * {@link AuthenticatedConnectionContext} for the current request using its
 * {@link SecurityContext}.
 */
final class InternalProxyAuthzFilter implements Filter
{
  private final IdentityMapper<?> identityMapper;
  private final Schema schema;
  private final Function<SecurityContext, String, LdapException> authzIdProvider;
  InternalProxyAuthzFilter(IdentityMapper<?> identityMapper, Schema schema,
      Function<SecurityContext, String, LdapException> authzIdProvider)
  {
    this.identityMapper = checkNotNull(identityMapper, "identityMapper cannot be null");
    this.schema = checkNotNull(schema, "schema cannot be null");
    this.authzIdProvider = checkNotNull(authzIdProvider, "authzIdProvider cannot be null");
  }
  @Override
  public final Promise<Response, NeverThrowsException> filter(Context context, Request request, Handler next)
  {
    final SecurityContext securityContext = context.asContext(SecurityContext.class);
    final LDAPContext ldapContext = context.asContext(LDAPContext.class);
    Connection tmp = null;
    try
    {
      tmp = ldapContext.getInternalConnectionFactory().getConnection(getUserDN(securityContext));
    }
    catch (LdapException | DirectoryException e)
    {
      closeSilently(tmp);
      return asErrorResponse(e);
    }
    final Connection authConnection = tmp;
    return next.handle(new AuthenticatedConnectionContext(context, authConnection), request)
               .thenFinally(new Runnable()
               {
                 @Override
                 public void run()
                 {
                   closeSilently(authConnection);
                 }
               });
  }
  private DN getUserDN(final SecurityContext securityContext) throws LdapException, DirectoryException
  {
    final String authzId = authzIdProvider.apply(securityContext);
    if (authzId.startsWith("u:"))
    {
      final Entry entry = identityMapper.getEntryForID(authzId);
      if (entry == null)
      {
        throw LdapException.newLdapException(ResultCode.INVALID_CREDENTIALS);
      }
      return entry.getName();
    }
    else if (authzId.startsWith("dn:"))
    {
      try
      {
        return DN.valueOf(authzId.substring(3), schema);
      }
      catch (LocalizedIllegalArgumentException e)
      {
        throw LdapException.newLdapException(ResultCode.INVALID_DN_SYNTAX, e);
      }
    }
    throw LdapException.newLdapException(ResultCode.AUTHORIZATION_DENIED);
  }
  static Promise<Response, NeverThrowsException> asErrorResponse(final Throwable t)
  {
    final ResourceException e = asResourceException(t);
    final Response response =
        new Response().setStatus(Status.valueOf(e.getCode())).setEntity(e.toJsonValue().getObject());
    return Promises.newResultPromise(response);
  }
}
opendj-server-legacy/src/main/java/org/opends/server/protocols/http/rest2ldap/Rest2LdapEmbeddedHttpApplication.java
File was deleted
opendj-server-legacy/src/main/java/org/opends/server/protocols/http/rest2ldap/Rest2LdapEndpoint.java
@@ -23,26 +23,39 @@
import java.net.URISyntaxException;
import java.net.URL;
import org.forgerock.http.Filter;
import org.forgerock.http.HttpApplication;
import org.forgerock.opendj.adapter.server3x.Adapters;
import org.forgerock.opendj.ldap.ConnectionFactory;
import org.forgerock.opendj.ldap.LdapException;
import org.forgerock.opendj.ldap.schema.Schema;
import org.forgerock.opendj.rest2ldap.Rest2LDAPHttpApplication;
import org.forgerock.opendj.server.config.server.Rest2ldapEndpointCfg;
import org.forgerock.services.context.SecurityContext;
import org.forgerock.util.Function;
import org.opends.server.api.HttpEndpoint;
import org.opends.server.core.DirectoryServer;
import org.opends.server.core.ServerContext;
import org.opends.server.types.InitializationException;
/**
 * Encapsulates configuration required to start a rest2ldap application in an
 * OpenDJ context. Acts as a factory for {@link Rest2LdapEmbeddedHttpApplication}.
 * Encapsulates configuration required to start a REST2LDAP application embedded
 * in this LDAP server. Acts as a factory for {@link Rest2LDAPHttpApplication}.
 */
public final class Rest2LdapEndpoint extends HttpEndpoint<Rest2ldapEndpointCfg>
{
  /**
   * Create a new Rest2LdapEnpoint with the supplied configuration.
   *
   * @param configuration
   *          Configuration to use for the {@link HttpApplication}
   * @param serverContext
   *          Server of this LDAP server
   */
  public Rest2LdapEndpoint(Rest2ldapEndpointCfg configuration)
  public Rest2LdapEndpoint(Rest2ldapEndpointCfg configuration, ServerContext serverContext)
  {
    super(configuration);
    super(configuration, serverContext);
  }
  @Override
@@ -53,7 +66,7 @@
      final URI configURI = new URI(configuration.getConfigUrl());
      final URL absoluteConfigUrl =
          configURI.isAbsolute() ? configURI.toURL() : getFileForPath(configuration.getConfigUrl()).toURI().toURL();
      return new Rest2LdapEmbeddedHttpApplication(absoluteConfigUrl, configuration.isAuthenticationRequired());
      return new InternalRest2LDAPHttpApplication(absoluteConfigUrl, serverContext.getSchemaNG());
    }
    catch (MalformedURLException | URISyntaxException e)
    {
@@ -62,4 +75,31 @@
    }
  }
  /**
   * Specialized {@link Rest2LDAPHttpApplication} using internal connections to
   * this local LDAP server.
   */
  private final class InternalRest2LDAPHttpApplication extends Rest2LDAPHttpApplication
  {
    private final ConnectionFactory rootInternalConnectionFactory = Adapters.newRootConnectionFactory();
    InternalRest2LDAPHttpApplication(URL configURL, Schema schema)
    {
      super(configURL, schema);
    }
    @Override
    protected Filter newProxyAuthzFilter(final ConnectionFactory connectionFactory,
        final Function<SecurityContext, String, LdapException> authzIdProvider)
    {
      return new InternalProxyAuthzFilter(DirectoryServer.getProxiedAuthorizationIdentityMapper(), schema,
          authzIdProvider);
    }
    @Override
    protected ConnectionFactory getConnectionFactory(String name)
    {
      return rootInternalConnectionFactory;
    }
  }
}
opendj-server-legacy/src/test/java/org/forgerock/opendj/adapter/server3x/AdaptersTestCase.java
@@ -65,6 +65,8 @@
import org.forgerock.util.Options;
import org.opends.server.DirectoryServerTestCase;
import org.opends.server.TestCaseUtils;
import org.opends.server.protocols.internal.InternalClientConnection;
import org.opends.server.types.DirectoryException;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
@@ -76,18 +78,6 @@
public class AdaptersTestCase extends DirectoryServerTestCase {
    private static final String USER_0_DN_STRING = "uid=user.0,o=test";
    /**
     * Provides an anonymous connection factories.
     *
     * @return Anonymous connection factories.
     */
    @DataProvider
    public Object[][] anonymousConnectionFactories() {
        return new Object[][] {
            { new LDAPConnectionFactory("localhost", getServerLdapPort()) },
            { Adapters.newAnonymousConnectionFactory() } };
    }
    private Integer getServerLdapPort() {
        return TestCaseUtils.getServerLdapPort();
    }
@@ -96,9 +86,10 @@
     * Provides root connection factories.
     *
     * @return Root connection factories.
     * @throws DirectoryException
     */
    @DataProvider
    public Object[][] rootConnectionFactories() {
    public Object[][] rootConnectionFactories() throws DirectoryException {
        return new Object[][] {
            { new LDAPConnectionFactory("localhost",
                                        getServerLdapPort(),
@@ -106,7 +97,8 @@
                                               .set(AUTHN_BIND_REQUEST,
                                                    newSimpleBindRequest("cn=directory manager",
                                                                                  "password".toCharArray()))) },
            { Adapters.newConnectionFactoryForUser(DN.valueOf("cn=directory manager")) } };
            { Adapters.newConnectionFactory(new InternalClientConnection(DN.valueOf("cn=directory manager"))) },
            { Adapters.newRootConnectionFactory() } };
    }
    /**
@@ -120,7 +112,7 @@
        TestCaseUtils.startServer();
        // Creates a root connection to add data
        final Connection connection = Adapters.newRootConnection();
        final Connection connection = Adapters.newRootConnectionFactory().getConnection();
        // @formatter:off
        connection.add(
                "dn: uid=user.0, o=test",
@@ -211,8 +203,8 @@
     */
    @Test
    public void testSimpleLDAPConnectionFactorySimpleBind() throws LdapException {
        final LDAPConnectionFactory factory = new LDAPConnectionFactory("localhost", getServerLdapPort());
        try (Connection connection = factory.getConnection()) {
        try (final LDAPConnectionFactory factory = new LDAPConnectionFactory("localhost", getServerLdapPort());
             final Connection connection = factory.getConnection()) {
            connection.bind("cn=Directory Manager", "password".toCharArray());
            assertThat(connection.isValid()).isTrue();
            assertThat(connection.isClosed()).isFalse();
@@ -228,12 +220,9 @@
     */
    @Test
    public void testLDAPSASLBind() throws NumberFormatException, GeneralSecurityException, LdapException {
        LDAPConnectionFactory factory = new LDAPConnectionFactory("localhost", getServerLdapPort());
        PlainSASLBindRequest request =
                Requests.newPlainSASLBindRequest("u:user.0", "password".toCharArray());
        try (Connection connection = factory.getConnection()) {
        final PlainSASLBindRequest request = Requests.newPlainSASLBindRequest("u:user.0", "password".toCharArray());
        try (final LDAPConnectionFactory factory = new LDAPConnectionFactory("localhost", getServerLdapPort());
             final Connection connection = factory.getConnection()) {
            connection.bind(request);
        }
    }
@@ -244,36 +233,22 @@
     * @throws LdapException
     */
    @Test
    public void testAdapterConnectionSASLBindRequest() throws LdapException,
            GeneralSecurityException {
    public void testAdapterConnectionSASLBindRequest() throws LdapException {
        PlainSASLBindRequest request =
                Requests.newPlainSASLBindRequest("u:user.0", "password".toCharArray());
        try (final Connection connection = Adapters.newRootConnection()) {
        try (final Connection connection = Adapters.newRootConnectionFactory().getConnection()) {
            connection.bind(request);
        }
    }
    /**
     * This type of connection is not supported. Anonymous SASL Mechanisms is
     * disabled in the config.ldif file.
     *
     * @throws LdapException
     */
    @Test(dataProvider = "anonymousConnectionFactories", expectedExceptions = LdapException.class)
    public void testConnectionAnonymousSASLBindRequest(final ConnectionFactory factory) throws LdapException {
        try (final Connection connection = factory.getConnection()) {
            connection.bind(Requests.newAnonymousSASLBindRequest("anonymousSASLBindRequest"));
        }
    }
    /**
     * Binds as a root.
     *
     * @throws Exception
     */
    @Test
    public void testAdapterConnectionSimpleBindAsRoot() throws Exception {
        try (final Connection connection = Adapters.newRootConnection()) {
        try (final Connection connection = Adapters.newRootConnectionFactory().getConnection()) {
            final BindResult result = connection.bind("cn=Directory Manager", "password".toCharArray());
            assertThat(connection.isValid()).isTrue();
            assertThat(result.getResultCode()).isEqualTo(ResultCode.SUCCESS);
@@ -287,7 +262,8 @@
     */
    @Test
    public void testAdapterConnectionSimpleBindAsAUser() throws Exception {
        try (final Connection connection = Adapters.newConnectionForUser(DN.valueOf(USER_0_DN_STRING))) {
        try (final Connection connection = Adapters.newConnectionFactory(
                new InternalClientConnection(DN.valueOf(USER_0_DN_STRING))).getConnection()) {
            final BindResult result = connection.bind(USER_0_DN_STRING, "password".toCharArray());
            assertThat(result.getResultCode()).isEqualTo(ResultCode.SUCCESS);
        }
@@ -300,34 +276,21 @@
     */
    @Test(expectedExceptions = AuthenticationException.class)
    public void testAdapterConnectionSimpleBindAsAUserWrongPassword() throws Exception {
        try (final Connection connection = Adapters.newConnectionForUser(DN.valueOf(USER_0_DN_STRING))) {
        try (final Connection connection =  Adapters.newConnectionFactory(
                new InternalClientConnection(DN.valueOf(USER_0_DN_STRING))).getConnection()) {
            // Invalid credentials
            connection.bind(USER_0_DN_STRING, "pass".toCharArray());
        }
    }
    /**
     * Tries to bind as anonymous.
     *
     * @throws Exception
     */
    @Test
    public void testAdapterConnectionSimpleBind() throws Exception {
        // Anonymous
        try (final Connection connection = Adapters.newAnonymousConnection()) {
            final BindResult result = connection.bind("", "".toCharArray());
            assertThat(result.getDiagnosticMessage()).isEmpty();
        }
    }
    /**
     * Testing the adapters with a simple add request.
     *
     * @throws Exception
     */
    @Test
    public void testAdapterAddRequest() throws Exception {
        final Connection connection = Adapters.newRootConnection();
        final Connection connection = Adapters.newRootConnectionFactory().getConnection();
        // @formatter:off
        final AddRequest addRequest = Requests.newAddRequest(
                "dn: sn=carter,o=test",
@@ -383,7 +346,7 @@
     */
    @Test
    public void testAdapterSearchRequest() throws Exception {
        final Connection connection = Adapters.newRootConnection();
        final Connection connection = Adapters.newRootConnectionFactory().getConnection();
        final SearchRequest request =
                Requests.newSearchRequest("o=test", SearchScope.WHOLE_SUBTREE,
@@ -564,7 +527,7 @@
     */
    @Test
    public void testAdapterDeleteRequest() throws LdapException {
        try (final Connection connection = Adapters.newRootConnection()) {
        try (final Connection connection = Adapters.newRootConnectionFactory().getConnection()) {
            // Checks if the entry exists.
            SearchResultEntry sre =
                    connection.searchSingleEntry(Requests.newSearchRequest(
@@ -598,7 +561,7 @@
                        PreReadRequestControl.newControl(true, "mail")).addModification(
                        ModificationType.ADD, "mail", "modified@example.com");
        final Connection connection = Adapters.newRootConnection();
        final Connection connection = Adapters.newRootConnectionFactory().getConnection();
        final Result result = connection.modify(changeRequest);
        assertThat(result.getDiagnosticMessage()).isEmpty();
        assertThat(result.getControls()).isNotEmpty();
@@ -746,150 +709,6 @@
    }
    /**
     * If an anonymous tries to delete, sends a result code : insufficient
     * access rights.
     *
     * @throws LdapException
     */
    @Test(dataProvider = "anonymousConnectionFactories",
            expectedExceptions = AuthorizationException.class)
    public void testAdapterAsAnonymousCannotPerformDeleteRequest(final ConnectionFactory factory)
            throws LdapException {
        final DeleteRequest deleteRequest =
                Requests.newDeleteRequest("uid=user.2,o=test");
        try (final Connection connection = factory.getConnection()) {
            connection.delete(deleteRequest);
        }
    }
    /**
     * If an anonymous tries to do an add request, sends a result code :
     * insufficient access rights.
     *
     * @throws LdapException
     */
    @Test(dataProvider = "anonymousConnectionFactories",
            expectedExceptions = AuthorizationException.class)
    public void testAdapterAsAnonymousCannotPerformAddRequest(final ConnectionFactory factory)
            throws LdapException {
        // @formatter:off
        final AddRequest addRequest = Requests.newAddRequest(
                "dn: sn=scarter,o=test",
                "objectClass: top",
                "objectClass: person",
                "cn: scarter");
        // @formatter:on
        try (final Connection connection = factory.getConnection()) {
            connection.add(addRequest);
        }
    }
    /**
     * If an anonymous tries to do a modify DN request, sends a result code :
     * insufficient access rights.
     *
     * @throws LdapException
     */
    @Test(dataProvider = "anonymousConnectionFactories",
            expectedExceptions = AuthorizationException.class)
    public void testAdapterAsAnonymousCannotPerformModifyDNRequest(final ConnectionFactory factory)
            throws LdapException {
        final ModifyDNRequest changeRequest =
                Requests.newModifyDNRequest("uid=user.2,o=test", "uid=user.test")
                        .setDeleteOldRDN(true);
        try (final Connection connection = factory.getConnection()) {
            connection.modifyDN(changeRequest);
        }
    }
    /**
     * If an anonymous tries to do a modify request, sends a result code :
     * insufficient access rights.
     *
     * @throws LdapException
     */
    @Test(dataProvider = "anonymousConnectionFactories",
            expectedExceptions = LdapException.class)
    public void testAdapterAsAnonymousCannotPerformModifyRequest(final ConnectionFactory factory)
            throws LdapException {
        final ModifyRequest changeRequest =
                Requests.newModifyRequest("uid=user.2,o=test").addControl(
                        PreReadRequestControl.newControl(true, "mail")).addModification(
                        ModificationType.REPLACE, "mail", "modified@example.com");
        try (final Connection connection = factory.getConnection()) {
            connection.modify(changeRequest);
        }
    }
    /**
     * The anonymous connection is allowed to perform compare request.
     *
     * @throws LdapException
     */
    @Test(dataProvider = "anonymousConnectionFactories")
    public void testAdapterAsAnonymousPerformsCompareRequest(final ConnectionFactory factory)
            throws LdapException {
        final CompareRequest compareRequest =
                Requests.newCompareRequest(USER_0_DN_STRING, "uid", "user.0");
        try (final Connection connection = factory.getConnection()) {
            final CompareResult result = connection.compare(compareRequest);
            assertThat(result.getResultCode()).isEqualTo(ResultCode.COMPARE_TRUE);
            assertThat(result.getDiagnosticMessage()).isEmpty();
            assertThat(result.getControls()).isEmpty();
            assertThat(result.getMatchedDN()).isEmpty();
        }
    }
    /**
     * The anonymous connection is allowed to perform search request.
     *
     * @throws Exception
     */
    @Test(dataProvider = "anonymousConnectionFactories")
    public void testAdapterAsAnonymousPerformsSearchRequest(final ConnectionFactory factory)
            throws Exception {
        final SearchRequest request =
                Requests.newSearchRequest("o=test", SearchScope.WHOLE_SUBTREE,
                        "(uid=user.1)");
        final Connection connection = factory.getConnection();
        final ConnectionEntryReader reader = connection.search(request);
        assertThat(reader.isEntry()).isTrue();
        final SearchResultEntry entry = reader.readEntry();
        assertThat(entry).isNotNull();
        assertThat(entry.getName().toString()).isEqualTo("uid=user.1,o=test");
        assertThat(reader.hasNext()).isFalse();
    }
    /**
     * The anonymous connection is not allowed to perform search request
     * associated with a control.
     * <p>
     * Unavailable Critical Extension: The request control with Object
     * Identifier (OID) "x.x.x" cannot be used due to insufficient access
     * rights.
     *
     * @throws Exception
     */
    @Test(dataProvider = "anonymousConnectionFactories", expectedExceptions = LdapException.class)
    public void testAdapterAsAnonymousCannotPerformSearchRequestWithControl(
            final ConnectionFactory factory) throws Exception {
        final Connection connection = factory.getConnection();
        final SearchRequest request =
                Requests.newSearchRequest("o=test", SearchScope.WHOLE_SUBTREE,
                        "(uid=user.1)").addControl(ADNotificationRequestControl.newControl(true));
        final ConnectionEntryReader reader = connection.search(request);
        reader.readEntry();
    }
    /**
     * Creates an LDAP Connection and performs some basic calls like
     * add/delete/search and compare results with an SDK adapter connection
     * doing the same.
@@ -931,7 +750,7 @@
        assertThat(deleteResult.getResultCode()).isEqualTo(ResultCode.SUCCESS);
        // SDK Adapter connection
        final Connection adapterConnection = Adapters.newRootConnection();
        final Connection adapterConnection = Adapters.newRootConnectionFactory().getConnection();
        final Result sdkAddResult = adapterConnection.add(addRequest);
        final ConnectionEntryReader sdkReader = adapterConnection.search(searchRequest);
        final Result sdkDeleteResult = adapterConnection.delete(deleteRequest);
opendj-server-legacy/src/test/java/org/opends/server/protocols/http/AuthenticationFilterTest.java
File was deleted