From 9020a676bbe359cb158e96761ef6f1a3c32c80e5 Mon Sep 17 00:00:00 2001
From: Yannick Lecaillez <yannick.lecaillez@forgerock.com>
Date: Tue, 10 May 2016 16:42:27 +0000
Subject: [PATCH] REST2LDAP Refactoring
---
opendj-server-legacy/src/main/java/org/opends/server/protocols/http/LDAPContext.java | 54
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Config.java | 60
opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java | 196 +
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/package-info.java | 9
opendj-server-legacy/src/main/java/org/opends/server/core/DirectoryServer.java | 9
opendj-server-legacy/src/main/java/org/opends/server/protocols/http/rest2ldap/Rest2LdapEndpoint.java | 50
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/DirectConnectionFilter.java | 75
opendj-rest2ldap/pom.xml | 2
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/AbstractAsynchronousConnectionDecorator.java | 130 +
opendj-server-legacy/resource/config/http-config.json | 291 +-
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAPHttpApplication.java | 402 +++
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/AuthzIdTemplate.java | 57
opendj-server-legacy/src/main/java/org/opends/server/protocols/http/rest2ldap/InternalProxyAuthzFilter.java | 126 +
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/Utils.java | 54
opendj-server-legacy/src/main/java/org/opends/server/protocols/http/LDAPContextInjectionFilter.java | 81
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/ProxiedAuthV2Filter.java | 223 ++
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/SearchThenBindStrategy.java | 101 +
opendj-server-legacy/src/main/java/org/opends/server/api/HttpEndpoint.java | 11
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/AuthenticationStrategy.java | 44
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/OptionalFilter.java | 71
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthenticatedConnectionContext.java | 4
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/HttpBasicAuthenticationFilter.java | 184 ++
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/CachedReadConnectionDecorator.java | 258 ++
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/SimpleBindStrategy.java | 95 +
opendj-server-legacy/src/main/java/org/forgerock/opendj/adapter/server3x/Adapters.java | 449 ++--
opendj-server-legacy/src/test/java/org/forgerock/opendj/adapter/server3x/AdaptersTestCase.java | 229 --
opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/OptionalFilterTest.java | 65
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java | 223 +-
opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/HttpBasicAuthenticationFilterTest.java | 121 +
opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/ProxiedAuthV2FilterTest.java | 202 ++
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLDAPAttributeMapper.java | 29
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java | 39
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java | 17
/dev/null | 135 -
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java | 9
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java | 545 ++---
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java | 94
opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/opendj-rest2ldap-config.json | 358 +--
opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/AuthzIdTemplateTest.java | 5
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AttributeMapper.java | 44
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/SASLPlainStrategy.java | 133 +
opendj-server-legacy/src/main/java/org/opends/server/core/HttpEndpointConfigManager.java | 16
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/NameStrategy.java | 27
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/package-info.java | 25
opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/DirectConnectionFilterTest.java | 89
45 files changed, 3,667 insertions(+), 1,774 deletions(-)
diff --git a/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/opendj-rest2ldap-config.json b/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/opendj-rest2ldap-config.json
index 5e5d744..830a126 100644
--- a/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/opendj-rest2ldap-config.json
+++ b/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" } }
+ } }
+ }
+ }
+ }
}
diff --git a/opendj-rest2ldap/pom.xml b/opendj-rest2ldap/pom.xml
index 8600474..842b3f2 100644
--- a/opendj-rest2ldap/pom.xml
+++ b/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>
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLDAPAttributeMapper.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLDAPAttributeMapper.java
index dcf269f..a59546a 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLDAPAttributeMapper.java
+++ b/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));
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AttributeMapper.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AttributeMapper.java
index d0fd363..0660502 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AttributeMapper.java
+++ b/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.
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthenticatedConnectionContext.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthenticatedConnectionContext.java
index b991193..bffbffe 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthenticatedConnectionContext.java
+++ b/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;
}
}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthorizationPolicy.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthorizationPolicy.java
deleted file mode 100644
index cb7d0fb..0000000
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthorizationPolicy.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * The contents of this file are subject to the terms of the Common Development and
- * Distribution License (the License). You may not use this file except in compliance with the
- * License.
- *
- * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
- * specific language governing permission and limitations under the License.
- *
- * When distributing Covered Software, include this CDDL Header Notice in each file and include
- * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
- * Header, with the fields enclosed by brackets [] replaced by your own identifying
- * information: "Portions copyright [year] [name of copyright owner]".
- *
- * Copyright 2013 ForgeRock AS.
- */
-
-package org.forgerock.opendj.rest2ldap;
-
-/**
- * The policy which should be for performing authorization.
- */
-public enum AuthorizationPolicy {
- /**
- * Use connections acquired from the LDAP connection factory. Don't use
- * proxied authorization, and don't use cached pre-authenticated
- * connections.
- */
- NONE,
-
- /**
- * Use the connection obtained during LDAP authentication. If no connection
- * was passed through the authorization will fail.
- */
- REUSE,
-
- /**
- * Use proxied authorization with an authorization ID derived from the
- * proxied authorization ID template. Proxied authorization will only be
- * used if there is no pre-authenticated connection available.
- */
- PROXY;
-}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Config.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Config.java
index 101e53d..8a98cb0 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Config.java
+++ b/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;
- }
}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/HttpAuthenticationFilter.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/HttpAuthenticationFilter.java
deleted file mode 100644
index 306cf3c..0000000
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/HttpAuthenticationFilter.java
+++ /dev/null
@@ -1,385 +0,0 @@
-/*
- * The contents of this file are subject to the terms of the Common Development and
- * Distribution License (the License). You may not use this file except in compliance with the
- * License.
- *
- * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
- * specific language governing permission and limitations under the License.
- *
- * When distributing Covered Software, include this CDDL Header Notice in each file and include
- * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
- * Header, with the fields enclosed by brackets [] replaced by your own identifying
- * information: "Portions copyright [year] [name of copyright owner]".
- *
- * Copyright 2013-2015 ForgeRock AS.
- */
-package org.forgerock.opendj.rest2ldap;
-
-import static org.forgerock.json.resource.ResourceException.newResourceException;
-import static org.forgerock.services.context.SecurityContext.AUTHZID_DN;
-import static org.forgerock.services.context.SecurityContext.AUTHZID_ID;
-import static org.forgerock.opendj.ldap.Connections.uncloseable;
-import static org.forgerock.opendj.ldap.LdapException.newLdapException;
-import static org.forgerock.opendj.ldap.requests.Requests.newPlainSASLBindRequest;
-import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest;
-import static org.forgerock.opendj.ldap.requests.Requests.newSimpleBindRequest;
-import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException;
-import static org.forgerock.util.Utils.closeSilently;
-
-import java.io.Closeable;
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.Map;
-import java.util.StringTokenizer;
-import java.util.concurrent.atomic.AtomicReference;
-
-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.json.JsonValue;
-import org.forgerock.json.JsonValueException;
-import org.forgerock.json.resource.ResourceException;
-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.ConnectionFactory;
-import org.forgerock.opendj.ldap.DN;
-import org.forgerock.opendj.ldap.EntryNotFoundException;
-import org.forgerock.opendj.ldap.Filter;
-import org.forgerock.opendj.ldap.LdapException;
-import org.forgerock.opendj.ldap.MultipleEntriesFoundException;
-import org.forgerock.opendj.ldap.ResultCode;
-import org.forgerock.opendj.ldap.SearchScope;
-import org.forgerock.opendj.ldap.requests.BindRequest;
-import org.forgerock.opendj.ldap.requests.Requests;
-import org.forgerock.opendj.ldap.responses.BindResult;
-import org.forgerock.opendj.ldap.responses.SearchResultEntry;
-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.promise.NeverThrowsException;
-import org.forgerock.util.promise.Promise;
-import org.forgerock.util.promise.Promises;
-
-/** An LDAP based HTTP authentication filter. */
-final class HttpAuthenticationFilter implements org.forgerock.http.Filter, Closeable {
-
- /** Indicates how authentication should be performed. */
- private enum AuthenticationMethod {
- SASL_PLAIN,
- SEARCH_SIMPLE,
- SIMPLE
- }
-
- private final Schema schema = Schema.getDefaultSchema();
- private final String altAuthenticationPasswordHeader;
- private final String altAuthenticationUsernameHeader;
- private final AuthenticationMethod authenticationMethod;
- private final ConnectionFactory bindLDAPConnectionFactory;
- private final boolean reuseAuthenticatedConnection;
- private final String saslAuthzIdTemplate;
- private final DN searchBaseDN;
- private final String searchFilterTemplate;
- private final ConnectionFactory searchLDAPConnectionFactory;
- private final SearchScope searchScope;
- private final boolean supportAltAuthentication;
-
- private final boolean supportHTTPBasicAuthentication;
-
- HttpAuthenticationFilter(final JsonValue configuration) {
- // Parse the authentication configuration.
- final JsonValue authnConfig = configuration.get("authenticationFilter");
- supportHTTPBasicAuthentication = authnConfig.get("supportHTTPBasicAuthentication").required().asBoolean();
-
- // Alternative HTTP authentication.
- supportAltAuthentication = authnConfig.get("supportAltAuthentication").required().asBoolean();
- if (supportAltAuthentication) {
- altAuthenticationUsernameHeader = authnConfig.get("altAuthenticationUsernameHeader").required().asString();
- altAuthenticationPasswordHeader = authnConfig.get("altAuthenticationPasswordHeader").required().asString();
- } else {
- altAuthenticationUsernameHeader = null;
- altAuthenticationPasswordHeader = null;
- }
-
- // Should the authenticated connection should be cached for use by subsequent LDAP operations?
- reuseAuthenticatedConnection = authnConfig.get("reuseAuthenticatedConnection").required().asBoolean();
-
- // Parse the authentication method and associated parameters.
- authenticationMethod = parseAuthenticationMethod(authnConfig);
- switch (authenticationMethod) {
- case SASL_PLAIN:
- saslAuthzIdTemplate = authnConfig.get("saslAuthzIdTemplate").required().asString();
- searchBaseDN = null;
- searchScope = null;
- searchFilterTemplate = null;
- searchLDAPConnectionFactory = null;
- break;
- case SEARCH_SIMPLE:
- searchBaseDN = DN.valueOf(authnConfig.get("searchBaseDN").required().asString(), schema);
- searchScope = parseSearchScope(authnConfig);
- searchFilterTemplate = authnConfig.get("searchFilterTemplate").required().asString();
-
- // Parse the LDAP connection factory to be used for searches.
- final String ldapFactoryName = authnConfig.get("searchLDAPConnectionFactory").required().asString();
- searchLDAPConnectionFactory = Rest2LDAP
- .configureConnectionFactory(configuration.get("ldapConnectionFactories").required(), ldapFactoryName);
-
- saslAuthzIdTemplate = null;
- break;
- case SIMPLE:
- default:
- saslAuthzIdTemplate = null;
- searchBaseDN = null;
- searchScope = null;
- searchFilterTemplate = null;
- searchLDAPConnectionFactory = null;
- break;
- }
-
- // Parse the LDAP connection factory to be used for binds.
- final String ldapFactoryName = authnConfig.get("bindLDAPConnectionFactory").required().asString();
- bindLDAPConnectionFactory = Rest2LDAP.configureConnectionFactory(
- configuration.get("ldapConnectionFactories").required(), ldapFactoryName);
- }
-
- private static AuthenticationMethod parseAuthenticationMethod(final JsonValue configuration) {
- if (configuration.isDefined("method")) {
- final String method = configuration.get("method").asString();
- if ("simple".equalsIgnoreCase(method)) {
- return AuthenticationMethod.SIMPLE;
- } else if ("sasl-plain".equalsIgnoreCase(method)) {
- return AuthenticationMethod.SASL_PLAIN;
- } else if ("search-simple".equalsIgnoreCase(method)) {
- return AuthenticationMethod.SEARCH_SIMPLE;
- } else {
- throw new JsonValueException(configuration,
- "Illegal authentication method: must be either 'simple', 'sasl-plain', or 'search-simple'");
- }
- } else {
- return AuthenticationMethod.SEARCH_SIMPLE;
- }
- }
-
- private static SearchScope parseSearchScope(final JsonValue configuration) {
- if (configuration.isDefined("searchScope")) {
- final String scope = configuration.get("searchScope").asString();
- if ("sub".equalsIgnoreCase(scope)) {
- return SearchScope.WHOLE_SUBTREE;
- } else if ("one".equalsIgnoreCase(scope)) {
- return SearchScope.SINGLE_LEVEL;
- } else {
- throw new JsonValueException(configuration, "Illegal search scope: must be either 'sub' or 'one'");
- }
- } else {
- return SearchScope.WHOLE_SUBTREE;
- }
- }
-
- @Override
- public Promise<Response, NeverThrowsException> filter(final Context context, final Request request,
- final Handler next) {
- // Store the authenticated connection so that it can be re-used by the handler if needed.
- // However, make sure that it is closed on completion.
- try {
- final String headerUsername =
- supportAltAuthentication ? request.getHeaders().getFirst(altAuthenticationUsernameHeader) : null;
- final String headerPassword =
- supportAltAuthentication ? request.getHeaders().getFirst(altAuthenticationPasswordHeader) : null;
- final String headerAuthorization =
- supportHTTPBasicAuthentication ? request.getHeaders().getFirst("Authorization") : null;
-
- final String username;
- final char[] password;
- if (headerUsername != null) {
- if (headerPassword == null || headerUsername.isEmpty() || headerPassword.isEmpty()) {
- throw newResourceException(401);
- }
- username = headerUsername;
- password = headerPassword.toCharArray();
- } else if (headerAuthorization != null) {
- final StringTokenizer st = new StringTokenizer(headerAuthorization);
- final String method = st.nextToken();
- if (method == null || !"BASIC".equalsIgnoreCase(method)) {
- throw newResourceException(401);
- }
- final String b64Credentials = st.nextToken();
- if (b64Credentials == null) {
- throw newResourceException(401);
- }
- final String credentials = ByteString.valueOfBase64(b64Credentials).toString();
- final String[] usernameAndPassword = credentials.split(":");
- if (usernameAndPassword.length != 2) {
- throw newResourceException(401);
- }
- username = usernameAndPassword[0];
- password = usernameAndPassword[1].toCharArray();
- } else {
- throw newResourceException(401);
- }
-
- // If we've got here then we have a username and password.
- switch (authenticationMethod) {
- case SIMPLE: {
- final Map<String, Object> authzid;
- authzid = new LinkedHashMap<>(2);
- authzid.put(AUTHZID_DN, username);
- authzid.put(AUTHZID_ID, username);
- return doBind(
- context, request, next, Requests.newSimpleBindRequest(username, password), username, authzid);
- }
- case SASL_PLAIN: {
- final Map<String, Object> authzid;
- final String bindId;
- if (saslAuthzIdTemplate.startsWith("dn:")) {
- final String bindDN = DN.format(saslAuthzIdTemplate.substring(3), schema, username).toString();
- bindId = "dn:" + bindDN;
- authzid = new LinkedHashMap<>(2);
- authzid.put(AUTHZID_DN, bindDN);
- authzid.put(AUTHZID_ID, username);
- } else {
- bindId = String.format(saslAuthzIdTemplate, username);
- authzid = Collections.singletonMap(AUTHZID_ID, (Object) username);
- }
- return doBind(context, request, next, newPlainSASLBindRequest(bindId, password), username, authzid);
- }
- default: // SEARCH_SIMPLE
- final AtomicReference<Connection> savedConnection = new AtomicReference<>();
- return searchLDAPConnectionFactory.getConnectionAsync()
- .thenAsync(doSearchForUser(username, savedConnection))
- .thenAsync(doBindAfterSearch(context, request, next, username, password, savedConnection),
- returnErrorAfterFailedSearch(savedConnection));
- }
- } catch (final Throwable t) {
- return asErrorResponse(t);
- }
- }
-
- private AsyncFunction<Connection, SearchResultEntry, LdapException> doSearchForUser(
- final String username, final AtomicReference<Connection> savedConnection) {
- return new AsyncFunction<Connection, SearchResultEntry, LdapException>() {
- @Override
- public Promise<SearchResultEntry, LdapException> apply(final Connection connection) throws LdapException {
- savedConnection.set(connection);
- final Filter filter = Filter.format(searchFilterTemplate, username);
- return connection.searchSingleEntryAsync(newSearchRequest(searchBaseDN, searchScope, filter, "1.1"));
- }
- };
- }
-
- private AsyncFunction<SearchResultEntry, Response, NeverThrowsException> doBindAfterSearch(
- final Context context, final Request request, final Handler next, final String username,
- final char[] password, final AtomicReference<Connection> savedConnection) {
- return new AsyncFunction<SearchResultEntry, Response, NeverThrowsException>() {
- @Override
- public Promise<Response, NeverThrowsException> apply(final SearchResultEntry entry) {
- closeConnection(savedConnection);
- final String bindDN = entry.getName().toString();
- final Map<String, Object> authzid = new LinkedHashMap<>(2);
- authzid.put(AUTHZID_DN, bindDN);
- authzid.put(AUTHZID_ID, username);
- return doBind(context, request, next, newSimpleBindRequest(bindDN, password), username, authzid);
- }
- };
- }
-
- /**
- * Get a bind connection and then perform the bind operation, setting the cached connection and authorization
- * credentials on completion.
- */
- private Promise<Response, NeverThrowsException> doBind(
- final Context context, final Request request, final Handler next, final BindRequest bindRequest,
- final String authcid, final Map<String, Object> authzid) {
- final AtomicReference<Connection> savedConnection = new AtomicReference<>();
- return bindLDAPConnectionFactory.getConnectionAsync()
- .thenAsync(new AsyncFunction<Connection, BindResult, LdapException>() {
- @Override
- public Promise<BindResult, LdapException> apply(final Connection connection) throws LdapException {
- savedConnection.set(connection);
- return connection.bindAsync(bindRequest);
- }
- })
- .thenAsync(doChain(context, request, next, authcid, authzid, savedConnection),
- returnErrorAfterFailedBind())
- .thenFinally(new Runnable() {
- @Override
- public void run() {
- closeConnection(savedConnection);
- }
- });
- }
-
- private AsyncFunction<BindResult, Response, NeverThrowsException> doChain(
- final Context context, final Request request, final Handler next, final String authcid,
- final Map<String, Object> authzid, final AtomicReference<Connection> savedConnection) {
- return new AsyncFunction<BindResult, Response, NeverThrowsException>() {
- @Override
- public Promise<Response, NeverThrowsException> apply(final BindResult result) {
- // Pass through the authentication ID and authorization principals.
- Context forwardedContext = new SecurityContext(context, authcid, authzid);
-
- // Cache the pre-authenticated connection and prevent downstream
- // components from closing it since this filter will close it.
- if (reuseAuthenticatedConnection) {
- forwardedContext = new AuthenticatedConnectionContext(
- forwardedContext, uncloseable(savedConnection.get()));
- }
-
- return next.handle(forwardedContext, request);
- }
- };
- }
-
- private AsyncFunction<LdapException, Response, NeverThrowsException> returnErrorAfterFailedSearch(
- final AtomicReference<Connection> savedConnection) {
- return new AsyncFunction<LdapException, Response, NeverThrowsException>() {
- @Override
- public Promise<Response, NeverThrowsException> apply(final LdapException e) {
- if (closeConnection(savedConnection)) {
- // The search error should not be passed as-is back to the user.
- if (e instanceof EntryNotFoundException || e instanceof MultipleEntriesFoundException) {
- return asErrorResponse(newLdapException(ResultCode.INVALID_CREDENTIALS, e));
- } else if (e instanceof AuthenticationException || e instanceof AuthorizationException) {
- return asErrorResponse(newLdapException(ResultCode.CLIENT_SIDE_LOCAL_ERROR, e));
- } else {
- return asErrorResponse(e);
- }
- } else {
- return asErrorResponse(e);
- }
- }
- };
- }
-
- private AsyncFunction<LdapException, Response, NeverThrowsException> returnErrorAfterFailedBind() {
- return new AsyncFunction<LdapException, Response, NeverThrowsException>() {
- @Override
- public Promise<Response, NeverThrowsException> apply(final LdapException e) {
- return asErrorResponse(e);
- }
- };
- }
-
- private 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);
- }
-
- private boolean closeConnection(final AtomicReference<Connection> savedConnection) {
- final Connection connection = savedConnection.get();
- if (connection != null) {
- connection.close();
- return true;
- }
- return false;
- }
-
- @Override
- public void close() {
- closeSilently(searchLDAPConnectionFactory, bindLDAPConnectionFactory);
- }
-}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java
index f916a0d..7d1f0a5 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java
+++ b/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)));
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java
index 382522b..c150a44 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java
+++ b/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));
+ }
+ };
+ }
}
}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/NameStrategy.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/NameStrategy.java
index 69e764e..ad7d817 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/NameStrategy.java
+++ b/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;
}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java
index 03cc53a..508d424 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java
+++ b/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)
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java
index cf0fd1c..140a94c 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java
+++ b/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));
+ }
+ });
}
}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/RequestState.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/RequestState.java
deleted file mode 100644
index 486d6f6..0000000
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/RequestState.java
+++ /dev/null
@@ -1,435 +0,0 @@
-/*
- * The contents of this file are subject to the terms of the Common Development and
- * Distribution License (the License). You may not use this file except in compliance with the
- * License.
- *
- * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
- * specific language governing permission and limitations under the License.
- *
- * When distributing Covered Software, include this CDDL Header Notice in each file and include
- * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
- * Header, with the fields enclosed by brackets [] replaced by your own identifying
- * information: "Portions Copyright [year] [name of copyright owner]".
- *
- * Copyright 2013-2016 ForgeRock AS.
- */
-package org.forgerock.opendj.rest2ldap;
-
-import java.io.Closeable;
-import java.util.LinkedHashMap;
-import java.util.Map;
-import java.util.concurrent.ConcurrentLinkedQueue;
-import java.util.concurrent.CountDownLatch;
-
-import org.forgerock.json.resource.InternalServerErrorException;
-import org.forgerock.json.resource.ResourceException;
-import org.forgerock.opendj.ldap.AbstractAsynchronousConnection;
-import org.forgerock.opendj.ldap.Connection;
-import org.forgerock.opendj.ldap.ConnectionEventListener;
-import org.forgerock.opendj.ldap.DN;
-import org.forgerock.opendj.ldap.LdapException;
-import org.forgerock.opendj.ldap.LdapPromise;
-import org.forgerock.opendj.ldap.IntermediateResponseHandler;
-import org.forgerock.opendj.ldap.LdapResultHandler;
-import org.forgerock.opendj.ldap.SearchResultHandler;
-import org.forgerock.opendj.ldap.SearchScope;
-import org.forgerock.opendj.ldap.controls.Control;
-import org.forgerock.opendj.ldap.controls.ProxiedAuthV2RequestControl;
-import org.forgerock.opendj.ldap.requests.AbandonRequest;
-import org.forgerock.opendj.ldap.requests.AddRequest;
-import org.forgerock.opendj.ldap.requests.BindRequest;
-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.Request;
-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;
-import org.forgerock.opendj.ldap.responses.SearchResultEntry;
-import org.forgerock.opendj.ldap.responses.SearchResultReference;
-import org.forgerock.services.context.Context;
-import org.forgerock.services.context.SecurityContext;
-import org.forgerock.util.promise.ExceptionHandler;
-import org.forgerock.util.promise.Promise;
-import org.forgerock.util.promise.PromiseImpl;
-import org.forgerock.util.promise.Promises;
-import org.forgerock.util.promise.ResultHandler;
-
-import static org.forgerock.opendj.rest2ldap.Rest2LDAP.*;
-import static org.forgerock.opendj.rest2ldap.Utils.*;
-
-/**
- * Common request information passed to containers and mappers.
- * A new @{code RequestState} is allocated for each REST request.
- */
-final class RequestState implements Closeable {
-
- /** 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;
- /** Guarded by 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);
- }
- }
- }
-
- /**
- * An LRU cache of recent reads requests. This is used in order to reduce
- * the number of repeated read operations performed when resolving DN
- * references.
- */
- @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;
- }
- };
-
- private final Config config;
- private final Context context;
- private Connection connection;
- private Control proxiedAuthzControl;
-
- RequestState(final Config config, final Context context) {
- this.config = config;
- this.context = context;
-
- /* Re-use the pre-authenticated connection if available and the authorization policy allows. */
- if (config.getAuthorizationPolicy() != AuthorizationPolicy.NONE
- && context.containsContext(AuthenticatedConnectionContext.class)) {
- this.connection = wrap(context.asContext(AuthenticatedConnectionContext.class).getConnection());
- } else {
- this.connection = null; // We'll allocate the connection.
- }
- }
-
- @Override
- public void close() {
- connection.close();
- }
-
- Config getConfig() {
- return config;
- }
-
- Context getContext() {
- return context;
- }
-
- /**
- * Performs common processing required before handling an HTTP request,
- * including calculating the proxied authorization request control. Then
- * return a promise containing a valid LDAP connection or a
- * {@link ResourceException} if an error is detected.
- * <p>
- * This method should be called at most once per request.
- * @return A {@link Promise} containing a valid {@link Connection}
- */
- Promise<Connection, ResourceException> getConnection() {
- /*
- * Compute the proxied authorization control from the content of the
- * security context if present. Only do this if we are not using a
- * cached connection since cached connections are supposed to have been
- * pre-authenticated and therefore do not require proxied authorization.
- */
- if (connection == null && config.getAuthorizationPolicy() == AuthorizationPolicy.PROXY) {
- if (context.containsContext(SecurityContext.class)) {
- try {
- final SecurityContext securityContext = context.asContext(SecurityContext.class);
- final String authzId = config.getProxiedAuthorizationTemplate().formatAsAuthzId(
- securityContext.getAuthorization(), config.schema());
- proxiedAuthzControl = ProxiedAuthV2RequestControl.newControl(authzId);
- } catch (final ResourceException e) {
- return Promises.newExceptionPromise(e);
- }
- } else {
- return Promises.<Connection, ResourceException> newExceptionPromise(new InternalServerErrorException(
- i18n("The request could not be authorized because it did not contain a security context")));
- }
- }
-
- /*
- * Now get the LDAP connection to use for processing subsequent LDAP
- * requests. A null factory indicates that Rest2LDAP has been configured
- * to re-use the LDAP connection which was used for authentication.
- */
- if (connection != null) {
- return Promises.newResultPromise(connection);
- } else if (config.connectionFactory() != null) {
- final PromiseImpl<Connection, ResourceException> promise = PromiseImpl.create();
- config.connectionFactory().getConnectionAsync().thenOnResult(new ResultHandler<Connection>() {
- @Override
- public final void handleResult(final Connection result) {
- connection = wrap(result);
- promise.handleResult(connection);
- }
- }).thenOnException(new ExceptionHandler<LdapException>() {
- @Override
- public final void handleException(final LdapException exception) {
- promise.handleException(asResourceException(exception));
- }
- });
- return promise;
- } else {
- return Promises.<Connection, ResourceException> newExceptionPromise(new InternalServerErrorException(
- i18n("The request could not be processed because there was no LDAP connection available for use")));
- }
- }
-
- /**
- * Adds read caching support to the provided connection as well
- * functionality which automatically adds the proxied authorization control
- * if needed.
- */
- private Connection wrap(final Connection connection) {
- /* We only use async methods so no need to wrap sync methods. */
- return new AbstractAsynchronousConnection() {
- @Override
- public LdapPromise<Void> abandonAsync(final AbandonRequest request) {
- return connection.abandonAsync(request);
- }
-
- @Override
- public LdapPromise<Result> addAsync(final AddRequest request,
- final IntermediateResponseHandler intermediateResponseHandler) {
- return connection.addAsync(withControls(request), intermediateResponseHandler);
- }
-
- @Override
- public void addConnectionEventListener(final ConnectionEventListener listener) {
- connection.addConnectionEventListener(listener);
- }
-
- @Override
- public LdapPromise<BindResult> bindAsync(final BindRequest request,
- final IntermediateResponseHandler intermediateResponseHandler) {
- /*
- * Simple brute force implementation in case the bind operation
- * modifies an entry: clear the cachedReads.
- */
- evictAll();
- return connection.bindAsync(request, intermediateResponseHandler);
- }
-
- @Override
- public void close() {
- connection.close();
- }
-
- @Override
- public void close(final UnbindRequest request, final String reason) {
- connection.close(request, reason);
- }
-
- @Override
- public LdapPromise<CompareResult> compareAsync(final CompareRequest request,
- final IntermediateResponseHandler intermediateResponseHandler) {
- return connection.compareAsync(withControls(request), intermediateResponseHandler);
- }
-
- @Override
- public LdapPromise<Result> deleteAsync(final DeleteRequest request,
- final IntermediateResponseHandler intermediateResponseHandler) {
- evict(request.getName());
- return connection.deleteAsync(withControls(request), intermediateResponseHandler);
- }
-
- @Override
- public <R extends ExtendedResult> LdapPromise<R> extendedRequestAsync(final ExtendedRequest<R> request,
- final IntermediateResponseHandler intermediateResponseHandler) {
- /*
- * Simple brute force implementation in case the extended
- * operation modifies an entry: clear the cachedReads.
- */
- evictAll();
- return connection.extendedRequestAsync(withControls(request), intermediateResponseHandler);
- }
-
- @Override
- public boolean isClosed() {
- return connection.isClosed();
- }
-
- @Override
- public boolean isValid() {
- return connection.isValid();
- }
-
- @Override
- public LdapPromise<Result> modifyAsync(final ModifyRequest request,
- final IntermediateResponseHandler intermediateResponseHandler) {
- evict(request.getName());
- return connection.modifyAsync(withControls(request), intermediateResponseHandler);
- }
-
- @Override
- public LdapPromise<Result> modifyDNAsync(final ModifyDNRequest request,
- final IntermediateResponseHandler intermediateResponseHandler) {
- // Simple brute force implementation: clear the cachedReads.
- evictAll();
- return connection.modifyDNAsync(withControls(request), intermediateResponseHandler);
- }
-
- @Override
- public void removeConnectionEventListener(final ConnectionEventListener listener) {
- connection.removeConnectionEventListener(listener);
- }
-
- /** Try and re-use a cached result if possible. */
- @Override
- public LdapPromise<Result> searchAsync(final SearchRequest request,
- final IntermediateResponseHandler intermediateResponseHandler, final 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 connection.searchAsync(withControls(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 = connection
- .searchAsync(withControls(request), intermediateResponseHandler, pendingCachedRead)
- .thenOnResult(pendingCachedRead).thenOnException(pendingCachedRead);
- pendingCachedRead.setPromise(promise);
- return promise;
- }
- }
-
- @Override
- public String toString() {
- return connection.toString();
- }
-
- private void evict(final DN name) {
- synchronized (cachedReads) {
- cachedReads.remove(name);
- }
- }
-
- private void evictAll() {
- synchronized (cachedReads) {
- cachedReads.clear();
- }
- }
-
- private <R extends Request> R withControls(final R request) {
- if (proxiedAuthzControl != null) {
- request.addControl(proxiedAuthzControl);
- }
- return request;
- }
- };
- }
-}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java
index 7163989..3f0a2ed 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java
+++ b/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);
}
/**
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAPHttpApplication.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAPHttpApplication.java
index 54a8b25..41a3b08 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAPHttpApplication.java
+++ b/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);
}
}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java
index 0035d55..b6d3d4a 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java
+++ b/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()) {
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/AbstractAsynchronousConnectionDecorator.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/AbstractAsynchronousConnectionDecorator.java
new file mode 100644
index 0000000..88ab33c
--- /dev/null
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/AbstractAsynchronousConnectionDecorator.java
@@ -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();
+ }
+}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/AuthenticationStrategy.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/AuthenticationStrategy.java
new file mode 100644
index 0000000..9101def
--- /dev/null
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/AuthenticationStrategy.java
@@ -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);
+}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthzIdTemplate.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/AuthzIdTemplate.java
similarity index 76%
rename from opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthzIdTemplate.java
rename to opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/AuthzIdTemplate.java
index 0ee94a6..6040d0a 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthzIdTemplate.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/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;
+ }
}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/CachedReadConnectionDecorator.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/CachedReadConnectionDecorator.java
new file mode 100644
index 0000000..d355612
--- /dev/null
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/CachedReadConnectionDecorator.java
@@ -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;
+ }
+ }
+}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/DirectConnectionFilter.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/DirectConnectionFilter.java
new file mode 100644
index 0000000..ecb187a
--- /dev/null
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/DirectConnectionFilter.java
@@ -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));
+ }
+}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/HttpBasicAuthenticationFilter.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/HttpBasicAuthenticationFilter.java
new file mode 100644
index 0000000..43c1aa8
--- /dev/null
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/HttpBasicAuthenticationFilter.java
@@ -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;
+ }
+ }
+}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/OptionalFilter.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/OptionalFilter.java
new file mode 100644
index 0000000..cc448ba
--- /dev/null
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/OptionalFilter.java
@@ -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);
+ }
+}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/ProxiedAuthV2Filter.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/ProxiedAuthV2Filter.java
new file mode 100644
index 0000000..7cd316c
--- /dev/null
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/ProxiedAuthV2Filter.java
@@ -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);
+ }
+ }
+}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/SASLPlainStrategy.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/SASLPlainStrategy.java
new file mode 100644
index 0000000..11d8e25
--- /dev/null
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/SASLPlainStrategy.java
@@ -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);
+ }
+ });
+ }
+}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/SearchThenBindStrategy.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/SearchThenBindStrategy.java
new file mode 100644
index 0000000..c0f1d00
--- /dev/null
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/SearchThenBindStrategy.java
@@ -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));
+ }
+ });
+ }
+}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/SimpleBindStrategy.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/SimpleBindStrategy.java
new file mode 100644
index 0000000..4d77253
--- /dev/null
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/SimpleBindStrategy.java
@@ -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);
+ }
+ });
+ }
+ };
+ }
+}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/Utils.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/Utils.java
new file mode 100644
index 0000000..a5cf1a8
--- /dev/null
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/Utils.java
@@ -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);
+ }
+}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/package-info.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/package-info.java
new file mode 100755
index 0000000..487ebfc
--- /dev/null
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/package-info.java
@@ -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;
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/package-info.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/package-info.java
index e92fb9e..8cbb3a1 100755
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/package-info.java
+++ b/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;
diff --git a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java
index 9c9d9c4..da7137b 100644
--- a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java
+++ b/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 =
diff --git a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/AuthzIdTemplateTest.java b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/AuthzIdTemplateTest.java
similarity index 96%
rename from opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/AuthzIdTemplateTest.java
rename to opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/AuthzIdTemplateTest.java
index b791036..3d7e099 100644
--- a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/AuthzIdTemplateTest.java
+++ b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/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());
diff --git a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/DirectConnectionFilterTest.java b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/DirectConnectionFilterTest.java
new file mode 100644
index 0000000..67903c6
--- /dev/null
+++ b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/DirectConnectionFilterTest.java
@@ -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));
+ }
+}
diff --git a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/HttpBasicAuthenticationFilterTest.java b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/HttpBasicAuthenticationFilterTest.java
new file mode 100644
index 0000000..a77a9a9
--- /dev/null
+++ b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/HttpBasicAuthenticationFilterTest.java
@@ -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));
+ }
+
+}
diff --git a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/OptionalFilterTest.java b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/OptionalFilterTest.java
new file mode 100644
index 0000000..bfac595
--- /dev/null
+++ b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/OptionalFilterTest.java
@@ -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));
+ }
+}
diff --git a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/ProxiedAuthV2FilterTest.java b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/ProxiedAuthV2FilterTest.java
new file mode 100644
index 0000000..66bd7d5
--- /dev/null
+++ b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/ProxiedAuthV2FilterTest.java
@@ -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;
+ }
+
+ }
+}
diff --git a/opendj-server-legacy/resource/config/http-config.json b/opendj-server-legacy/resource/config/http-config.json
index 5c6daa4..da8f01b 100644
--- a/opendj-server-legacy/resource/config/http-config.json
+++ b/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" } }
+ } }
}
}
}
diff --git a/opendj-server-legacy/src/main/java/org/forgerock/opendj/adapter/server3x/Adapters.java b/opendj-server-legacy/src/main/java/org/forgerock/opendj/adapter/server3x/Adapters.java
index 71875e6..eab6f55 100644
--- a/opendj-server-legacy/src/main/java/org/forgerock/opendj/adapter/server3x/Adapters.java
+++ b/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());
+ }
+ }
}
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/api/HttpEndpoint.java b/opendj-server-legacy/src/main/java/org/opends/server/api/HttpEndpoint.java
index 8fea4d9..e2336d8 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/api/HttpEndpoint.java
+++ b/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;
}
/**
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/core/DirectoryServer.java b/opendj-server-legacy/src/main/java/org/opends/server/core/DirectoryServer.java
index 7854287..ea44a46 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/core/DirectoryServer.java
+++ b/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();
}
}
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/core/HttpEndpointConfigManager.java b/opendj-server-legacy/src/main/java/org/opends/server/core/HttpEndpointConfigManager.java
index 200b782..5577b85 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/core/HttpEndpointConfigManager.java
+++ b/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)
{
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/AuthenticationFilter.java b/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/AuthenticationFilter.java
deleted file mode 100644
index 3293058..0000000
--- a/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/AuthenticationFilter.java
+++ /dev/null
@@ -1,346 +0,0 @@
-/*
- * The contents of this file are subject to the terms of the Common Development and
- * Distribution License (the License). You may not use this file except in compliance with the
- * License.
- *
- * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
- * specific language governing permission and limitations under the License.
- *
- * When distributing Covered Software, include this CDDL Header Notice in each file and include
- * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
- * Header, with the fields enclosed by brackets [] replaced by your own identifying
- * information: "Portions Copyright [year] [name of copyright owner]".
- *
- * Copyright 2013-2016 ForgeRock AS.
- */
-package org.opends.server.protocols.http;
-
-import static org.opends.messages.ProtocolMessages.*;
-import static org.opends.server.util.StaticUtils.*;
-
-import java.io.Closeable;
-import java.io.IOException;
-import java.nio.charset.Charset;
-import java.text.ParseException;
-
-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.LocalizableMessage;
-import org.forgerock.i18n.slf4j.LocalizedLogger;
-import org.forgerock.json.resource.ResourceException;
-import org.forgerock.opendj.adapter.server3x.Adapters;
-import org.forgerock.opendj.ldap.Connection;
-import org.forgerock.opendj.ldap.DN;
-import org.forgerock.opendj.ldap.Filter;
-import org.forgerock.opendj.ldap.LdapException;
-import org.forgerock.opendj.ldap.ResultCode;
-import org.forgerock.opendj.ldap.requests.BindRequest;
-import org.forgerock.opendj.ldap.requests.Requests;
-import org.forgerock.opendj.ldap.requests.SearchRequest;
-import org.forgerock.opendj.ldap.responses.BindResult;
-import org.forgerock.opendj.ldap.responses.SearchResultEntry;
-import org.forgerock.opendj.rest2ldap.AuthenticatedConnectionContext;
-import org.forgerock.opendj.rest2ldap.Rest2LDAP;
-import org.forgerock.services.context.ClientContext;
-import org.forgerock.services.context.Context;
-import org.forgerock.services.context.SecurityContext;
-import org.forgerock.util.AsyncFunction;
-import org.forgerock.util.promise.NeverThrowsException;
-import org.forgerock.util.promise.Promise;
-import org.forgerock.util.promise.Promises;
-import org.opends.server.schema.SchemaConstants;
-import org.opends.server.util.Base64;
-
-/** Servlet {@link Filter} that collects information about client connections. */
-public final class AuthenticationFilter implements org.forgerock.http.Filter, Closeable
-{
-
- /** HTTP Header sent by the client with HTTP basic authentication. */
- static final String HTTP_BASIC_AUTH_HEADER = "Authorization";
-
- /** The tracer object for the debug logger. */
- private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
-
- /**
- * Configures how to perform the search for the username prior to
- * authentication.
- */
- private final HTTPAuthenticationConfig authConfig;
-
- private final boolean authenticationRequired;
-
- /**
- * Constructs a new instance of this class.
- *
- * @param authenticationConfig
- * configures how to perform the search for the username prior to
- * authentication
- * @param authenticationRequired
- * If true, only authenticated requests will be accepted.
- */
- public AuthenticationFilter(HTTPAuthenticationConfig authenticationConfig, boolean authenticationRequired)
- {
- this.authConfig = authenticationConfig;
- this.authenticationRequired = authenticationRequired;
- }
-
- @Override
- public Promise<Response, NeverThrowsException> filter(Context context, Request request, Handler next)
- {
- try
- {
- final Connection ldapConnection = context.asContext(LDAPContext.class).getLdapConnectionFactory().getConnection();
- final String[] userCredentials = extractUsernamePassword(request);
- if (userCredentials != null && userCredentials.length == 2)
- {
- final String userName = userCredentials[0];
- final String password = userCredentials[1];
- return Adapters.newRootConnection()
- .searchSingleEntryAsync(buildSearchRequest(userName))
- .thenAsync(doBindAfterSearch(context, request, next, userName, password, ldapConnection),
- returnErrorAfterFailedSearch(context.asContext(ClientContext.class)));
- }
- else if (authenticationRequired)
- {
- return authenticationFailure(context.asContext(ClientContext.class));
- }
- else
- {
- // Use unauthenticated user
- return doFilter(context, request, next, ldapConnection);
- }
- }
- catch (Exception e)
- {
- return asErrorResponse(e, context.asContext(ClientContext.class));
- }
- }
-
- private SearchRequest buildSearchRequest(String userName)
- {
- // Use configured rights to find the user DN
- final Filter filter = Filter.format(authConfig.getSearchFilterTemplate(), userName);
- return Requests.newSearchRequest(
- authConfig.getSearchBaseDN(), authConfig.getSearchScope(), filter, SchemaConstants.NO_ATTRIBUTES);
- }
-
- private AsyncFunction<SearchResultEntry, Response, NeverThrowsException> doBindAfterSearch(final Context context,
- final Request request, final Handler next, final String userName, final String password,
- final Connection connection)
- {
- final ClientContext clientContext = context.asContext(ClientContext.class);
- return new AsyncFunction<SearchResultEntry, Response, NeverThrowsException>()
- {
- @Override
- public Promise<Response, NeverThrowsException> apply(final SearchResultEntry resultEntry)
- {
- final DN bindDN = resultEntry.getName();
-
- if (bindDN == null)
- {
- return authenticationFailure(clientContext);
- }
-
- final BindRequest bindRequest =
- Requests.newSimpleBindRequest(bindDN.toString(), password.getBytes(Charset.forName("UTF-8")));
- return connection.bindAsync(bindRequest)
- .thenAsync(doChain(context, request, next, userName, connection),
- returnErrorAfterFailedBind(clientContext));
- }
- };
- }
-
- private AsyncFunction<BindResult, Response, NeverThrowsException> doChain(final Context context,
- final Request request, final Handler next, final String userName, final Connection connection)
- {
- return new AsyncFunction<BindResult, Response, NeverThrowsException>()
- {
- @Override
- public Promise<Response, NeverThrowsException> apply(BindResult value) throws NeverThrowsException
- {
- try
- {
- SecurityContext securityContext = new SecurityContext(context, userName, null);
- return doFilter(securityContext, request, next, connection);
- }
- catch (Exception e)
- {
- return asErrorResponse(e, context.asContext(ClientContext.class));
- }
- }
- };
- }
-
- private Promise<Response, NeverThrowsException> doFilter(
- final Context context, final Request request, final Handler next, final Connection connection) throws Exception
- {
- final Context forwardedContext = new AuthenticatedConnectionContext(context, connection);
- // Send the request further down the filter chain or pass to servlet
- return next.handle(forwardedContext, request);
- }
-
- private AsyncFunction<? super LdapException, Response, NeverThrowsException> returnErrorAfterFailedSearch(
- final ClientContext clientContext)
- {
- return new AsyncFunction<LdapException, Response, NeverThrowsException>()
- {
- @Override
- public Promise<Response, NeverThrowsException> apply(final LdapException exception)
- {
- final ResultCode rc = exception.getResult().getResultCode();
- if (ResultCode.CLIENT_SIDE_NO_RESULTS_RETURNED.equals(rc)
- || ResultCode.CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED.equals(rc))
- {
- // Avoid information leak:
- // do not hint to the user that it is the username that is invalid
- return authenticationFailure(clientContext);
- }
- else
- {
- return asErrorResponse(exception, clientContext);
- }
- }
- };
- }
-
- private AsyncFunction<LdapException, Response, NeverThrowsException> returnErrorAfterFailedBind(
- final ClientContext clientContext)
- {
- return new AsyncFunction<LdapException, Response, NeverThrowsException>()
- {
- @Override
- public Promise<Response, NeverThrowsException> apply(final LdapException e)
- {
- return asErrorResponse(e, clientContext);
- }
- };
- }
-
- private Promise<Response, NeverThrowsException> authenticationFailure(final ClientContext clientContext)
- {
- return asErrorResponse(ResourceException.getException(401, "Invalid Credentials"), clientContext, false);
- }
-
- private Promise<Response, NeverThrowsException> asErrorResponse(final Throwable t, final ClientContext clientContext)
- {
- return asErrorResponse(t, clientContext, true);
- }
-
- private Promise<Response, NeverThrowsException> asErrorResponse(final Throwable t, final ClientContext clientContext,
- final boolean logError)
- {
- final ResourceException ex = Rest2LDAP.asResourceException(t);
- LocalizableMessage message = null;
- if (logError)
- {
- logger.traceException(ex);
- message =
- INFO_CONNHANDLER_UNABLE_TO_REGISTER_CLIENT.get(clientContext.getRemotePort(), clientContext.getLocalPort(),
- getExceptionMessage(ex));
- logger.debug(message);
- }
-
- return resourceExceptionToPromise(ex);
- }
-
- Promise<Response, NeverThrowsException> resourceExceptionToPromise(final ResourceException e)
- {
- final Response response = new Response().setStatus(Status.valueOf(e.getCode()))
- .setEntity(e.toJsonValue().getObject());
- if (e.getCode() == 401 && authConfig.isBasicAuthenticationSupported())
- {
- response.getHeaders().add("WWW-Authenticate", "Basic realm=\"org.forgerock.opendj\"");
- }
- return Promises.newResultPromise(response);
- }
-
- /**
- * Extracts the username and password from the request using one of the
- * enabled authentication mechanism: HTTP Basic authentication or HTTP Custom
- * headers. If no username and password can be obtained, then send back an
- * HTTP basic authentication challenge if HTTP basic authentication is
- * enabled.
- *
- * @param request
- * the request where to extract the username and password from
- * @return the array containing the username/password couple if both exist,
- * null otherwise
- * @throws ResourceException
- * if any error occur
- */
- String[] extractUsernamePassword(Request request) throws ResourceException
- {
- // TODO Use session to reduce hits with search + bind?
- // Use proxied authorization control for session.
-
- // Security: How can we remove the password held in the request headers?
- if (authConfig.isCustomHeadersAuthenticationSupported())
- {
- final String userName = request.getHeaders().getFirst(authConfig.getCustomHeaderUsername());
- final String password = request.getHeaders().getFirst(authConfig.getCustomHeaderPassword());
- if (userName != null && password != null)
- {
- return new String[] { userName, password };
- }
- }
-
- if (authConfig.isBasicAuthenticationSupported())
- {
- String httpBasicAuthHeader = request.getHeaders().getFirst(HTTP_BASIC_AUTH_HEADER);
- if (httpBasicAuthHeader != null)
- {
- String[] userCredentials = parseUsernamePassword(httpBasicAuthHeader);
- if (userCredentials != null)
- {
- return userCredentials;
- }
- }
- }
-
- return null;
- }
-
- /**
- * Parses username and password from the authentication header used in HTTP
- * basic authentication.
- *
- * @param authHeader
- * the authentication header obtained from the request
- * @return an array containing the username at index 0 and the password at
- * index 1, or null if the header cannot be parsed successfully
- * @throws ResourceException
- * if the base64 password cannot be decoded
- */
- String[] parseUsernamePassword(String authHeader) throws ResourceException
- {
- if (authHeader != null
- && (authHeader.startsWith("Basic") || authHeader.startsWith("basic")))
- {
- // We received authentication info
- // Example received header:
- // "Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="
- String base64UserCredentials = authHeader.substring("basic".length() + 1);
- try
- {
- // Example usage of base64:
- // Base64("Aladdin:open sesame") = "QWxhZGRpbjpvcGVuIHNlc2FtZQ=="
- String userCredentials = new String(Base64.decode(base64UserCredentials));
- String[] split = userCredentials.split(":");
- if (split.length == 2)
- {
- return split;
- }
- }
- catch (ParseException e)
- {
- throw Rest2LDAP.asResourceException(e);
- }
- }
- return null;
- }
-
- @Override
- public void close() throws IOException {}
-}
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/HTTPAuthenticationConfig.java b/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/HTTPAuthenticationConfig.java
deleted file mode 100644
index 3a83dcd..0000000
--- a/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/HTTPAuthenticationConfig.java
+++ /dev/null
@@ -1,222 +0,0 @@
-/*
- * The contents of this file are subject to the terms of the Common Development and
- * Distribution License (the License). You may not use this file except in compliance with the
- * License.
- *
- * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
- * specific language governing permission and limitations under the License.
- *
- * When distributing Covered Software, include this CDDL Header Notice in each file and include
- * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
- * Header, with the fields enclosed by brackets [] replaced by your own identifying
- * information: "Portions Copyright [year] [name of copyright owner]".
- *
- * Copyright 2013-2016 ForgeRock AS.
- */
-package org.opends.server.protocols.http;
-
-import org.forgerock.opendj.ldap.DN;
-import org.forgerock.opendj.ldap.SearchScope;
-
-/**
- * Class holding the configuration for HTTP authentication. This is extracted
- * from the JSON config file or the config held in LDAP.
- */
-public final class HTTPAuthenticationConfig
-{
-
- private boolean basicAuthenticationSupported;
- private boolean customHeadersAuthenticationSupported;
- private String customHeaderUsername;
- private String customHeaderPassword;
- private DN searchBaseDN;
- private SearchScope searchScope;
- private String searchFilterTemplate;
-
- /**
- * Returns whether HTTP basic authentication is supported.
- *
- * @return true if supported, false otherwise
- */
- public boolean isBasicAuthenticationSupported()
- {
- return basicAuthenticationSupported;
- }
-
- /**
- * Sets whether HTTP basic authentication is supported.
- *
- * @param supported
- * the supported value
- */
- public void setBasicAuthenticationSupported(boolean supported)
- {
- this.basicAuthenticationSupported = supported;
- }
-
- /**
- * Returns whether HTTP authentication via custom headers is supported.
- *
- * @return true if supported, false otherwise
- */
- public boolean isCustomHeadersAuthenticationSupported()
- {
- return customHeadersAuthenticationSupported;
- }
-
- /**
- * Sets whether HTTP authentication via custom headers is supported.
- *
- * @param supported
- * the supported value
- */
- public void setCustomHeadersAuthenticationSupported(boolean supported)
- {
- this.customHeadersAuthenticationSupported = supported;
- }
-
- /**
- * Returns the expected HTTP header for the username. This setting is only
- * used when HTTP authentication via custom headers is supported.
- *
- * @return the HTTP header for the username
- */
- public String getCustomHeaderUsername()
- {
- return customHeaderUsername;
- }
-
- /**
- * Sets the expected HTTP header for the username. This setting only takes
- * effect when HTTP authentication via custom headers is supported.
- *
- * @param customHeaderUsername
- * the HTTP header for the username
- */
- public void setCustomHeaderUsername(String customHeaderUsername)
- {
- this.customHeaderUsername = customHeaderUsername;
- }
-
- /**
- * Returns the expected HTTP header for the password. This setting is only
- * used when HTTP authentication via custom headers is supported.
- *
- * @return the HTTP header for the password
- */
- public String getCustomHeaderPassword()
- {
- return customHeaderPassword;
- }
-
- /**
- * Sets the expected HTTP header for the password. This setting only takes
- * effect when HTTP authentication via custom headers is supported.
- *
- * @param customHeaderPassword
- * the HTTP header for the password
- */
- public void setCustomHeaderPassword(String customHeaderPassword)
- {
- this.customHeaderPassword = customHeaderPassword;
- }
-
- /**
- * Returns the base DN to use when searching the entry corresponding to the
- * authenticating user.
- *
- * @return the base DN to use when searching the authenticating user
- */
- public DN getSearchBaseDN()
- {
- return searchBaseDN;
- }
-
- /**
- * Sets the base DN to use when searching the entry corresponding to the
- * authenticating user.
- *
- * @param searchBaseDN
- * the base DN to use when searching the authenticating user
- */
- public void setSearchBaseDN(DN searchBaseDN)
- {
- this.searchBaseDN = searchBaseDN;
- }
-
- /**
- * Returns the search scope to use when searching the entry corresponding to
- * the authenticating user.
- *
- * @return the search scope to use when searching the authenticating user
- */
- public SearchScope getSearchScope()
- {
- return searchScope;
- }
-
- /**
- * Sets the search scope to use when searching the entry corresponding to the
- * authenticating user.
- *
- * @param searchScope
- * the search scope to use when searching the authenticating user
- */
- public void setSearchScope(SearchScope searchScope)
- {
- this.searchScope = searchScope;
- }
-
- /**
- * Returns the search filter template to use when searching the entry
- * corresponding to the authenticating user.
- *
- * @return the search filter template to use when searching the authenticating
- * user
- */
- public String getSearchFilterTemplate()
- {
- return searchFilterTemplate;
- }
-
- /**
- * Sets the search filter template to use when searching the entry
- * corresponding to the authenticating user.
- *
- * @param searchFilterTemplate
- * the search filter template to use when searching the
- * authenticating user
- */
- public void setSearchFilterTemplate(String searchFilterTemplate)
- {
- this.searchFilterTemplate = searchFilterTemplate;
- }
-
- /** {@inheritDoc} */
- @Override
- public String toString()
- {
- StringBuilder sb = new StringBuilder();
- sb.append("basicAuth: ");
- if (!basicAuthenticationSupported)
- {
- sb.append("not ");
- }
- sb.append("supported, ");
- sb.append("customHeadersAuth: ");
- if (customHeadersAuthenticationSupported)
- {
- sb.append("usernameHeader=\"").append(customHeaderUsername).append("\",");
- sb.append("passwordHeader=\"").append(customHeaderPassword).append("\"");
- }
- else
- {
- sb.append("not supported, ");
- }
- sb.append("searchBaseDN: \"").append(searchBaseDN).append("\"");
- sb.append("searchScope: \"").append(searchScope).append("\"");
- sb.append("searchFilterTemplate: \"").append(searchFilterTemplate).append(
- "\"");
- return sb.toString();
- }
-}
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/LDAPContext.java b/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/LDAPContext.java
index 4c46a36..bb99b76 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/LDAPContext.java
+++ b/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;
+ }
+}
\ No newline at end of file
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/LDAPContextInjectionFilter.java b/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/LDAPContextInjectionFilter.java
index 30d4c99..a750273 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/LDAPContextInjectionFilter.java
+++ b/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);
}
-
-}
\ No newline at end of file
+}
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/rest2ldap/InternalProxyAuthzFilter.java b/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/rest2ldap/InternalProxyAuthzFilter.java
new file mode 100644
index 0000000..39a75c2
--- /dev/null
+++ b/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/rest2ldap/InternalProxyAuthzFilter.java
@@ -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);
+ }
+}
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/rest2ldap/Rest2LdapEmbeddedHttpApplication.java b/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/rest2ldap/Rest2LdapEmbeddedHttpApplication.java
deleted file mode 100644
index 79100ae..0000000
--- a/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/rest2ldap/Rest2LdapEmbeddedHttpApplication.java
+++ /dev/null
@@ -1,182 +0,0 @@
-/*
- * 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 2015-2016 ForgeRock AS.
- */
-package org.opends.server.protocols.http.rest2ldap;
-
-import static org.forgerock.http.util.Json.*;
-import static org.opends.messages.ProtocolMessages.*;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.URL;
-
-import org.forgerock.http.Handler;
-import org.forgerock.http.HttpApplication;
-import org.forgerock.http.HttpApplicationException;
-import org.forgerock.http.handler.Handlers;
-import org.forgerock.http.io.Buffer;
-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.ConnectionFactory;
-import org.forgerock.json.resource.RequestHandler;
-import org.forgerock.json.resource.ResourceException;
-import org.forgerock.json.resource.Resources;
-import org.forgerock.json.resource.Router;
-import org.forgerock.json.resource.http.CrestHttp;
-import org.forgerock.json.resource.http.HttpContextFactory;
-import org.forgerock.opendj.ldap.DN;
-import org.forgerock.opendj.ldap.SearchScope;
-import org.forgerock.opendj.rest2ldap.AuthorizationPolicy;
-import org.forgerock.opendj.rest2ldap.Rest2LDAP;
-import org.forgerock.services.context.Context;
-import org.forgerock.util.Factory;
-import org.forgerock.util.promise.NeverThrowsException;
-import org.forgerock.util.promise.Promise;
-import org.opends.server.protocols.http.AuthenticationFilter;
-import org.opends.server.protocols.http.HTTPAuthenticationConfig;
-
-/** Entry point of the Rest2Ldap application when used in embedded mode. */
-final class Rest2LdapEmbeddedHttpApplication implements HttpApplication
-{
- /**
- * Http Handler re-using the pre-established internal LDAP connection.
- *
- * @see AuthenticationFilter
- */
- private static final class Rest2LdapHandler implements Handler
- {
- private final Handler delegate;
-
- /**
- * Build a new {@code LdapHttpHandler}.
- *
- * @param configuration
- * The configuration which will be used to set
- * the connection and the mappings to the OpenDJ server.
- */
- public Rest2LdapHandler(final JsonValue configuration)
- {
- final ConnectionFactory connectionFactory = Resources.newInternalConnectionFactory(createRouter(configuration));
- delegate = CrestHttp.newHttpHandler(connectionFactory, new HttpContextFactory()
- {
- @Override
- public Context createContext(Context parentContext, Request request) throws ResourceException
- {
- return parentContext;
- }
- });
- }
-
- private RequestHandler createRouter(final JsonValue configuration)
- {
- final JsonValue mappings = configuration.get("servlet").get("mappings").required();
- final Router router = new Router();
-
- for (final String mappingUrl : mappings.keys()) {
- final JsonValue mapping = mappings.get(mappingUrl);
- final CollectionResourceProvider provider = Rest2LDAP.builder()
- .authorizationPolicy(AuthorizationPolicy.REUSE)
- .configureMapping(mapping)
- .build();
- router.addRoute(Router.uriTemplate(mappingUrl), provider);
- }
- return router;
- }
-
- @Override
- public final Promise<Response, NeverThrowsException> handle(final Context context, final Request request)
- {
- return delegate.handle(context, request);
- }
- }
-
- private final URL configFileUrl;
- private final boolean authenticationRequired;
-
- Rest2LdapEmbeddedHttpApplication(URL configFileUrl, boolean authenticationRequired)
- {
- this.configFileUrl = configFileUrl;
- this.authenticationRequired = authenticationRequired;
- }
-
- @Override
- public Handler start() throws HttpApplicationException
- {
- try
- {
- final Object jsonElems = readJson(configFileUrl);
- final JsonValue configuration = new JsonValue(jsonElems).recordKeyAccesses();
- final Handler handler = Handlers.chainOf(
- new Rest2LdapHandler(configuration),
- new AuthenticationFilter(getAuthenticationConfig(configuration), authenticationRequired));
- configuration.verifyAllKeysAccessed();
- return handler;
- }
- catch (final Exception e)
- {
- stop();
- throw new HttpApplicationException(ERR_INITIALIZE_HTTP_CONNECTION_HANDLER.get().toString(), e);
- }
- }
-
- private static JsonValue readJson(final URL resource) throws IOException
- {
- try (final InputStream in = resource.openStream())
- {
- return new JsonValue(readJsonLenient(in));
- }
- }
-
- private static HTTPAuthenticationConfig getAuthenticationConfig(final JsonValue configuration)
- {
- final HTTPAuthenticationConfig result = new HTTPAuthenticationConfig();
-
- final JsonValue val = configuration.get("authenticationFilter");
- result.setBasicAuthenticationSupported(asBool(val, "supportHTTPBasicAuthentication"));
- result.setCustomHeadersAuthenticationSupported(asBool(val, "supportAltAuthentication"));
- result.setCustomHeaderUsername(val.get("altAuthenticationUsernameHeader").asString());
- result.setCustomHeaderPassword(val.get("altAuthenticationPasswordHeader").asString());
-
- result.setSearchBaseDN(DN.valueOf(asString(val, "searchBaseDN")));
- result.setSearchScope(SearchScope.valueOf(asString(val, "searchScope")));
- result.setSearchFilterTemplate(asString(val, "searchFilterTemplate"));
-
- return result;
- }
-
- private static String asString(JsonValue value, String key)
- {
- return value.get(key).required().asString();
- }
-
- private static boolean asBool(JsonValue value, String key)
- {
- return value.get(key).defaultTo(false).asBoolean();
- }
-
- @Override
- public Factory<Buffer> getBufferFactory()
- {
- return null;
- }
-
- @Override
- public void stop()
- {
- // Nothing to do
- }
-}
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/rest2ldap/Rest2LdapEndpoint.java b/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/rest2ldap/Rest2LdapEndpoint.java
index e4f4da4..706347e 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/rest2ldap/Rest2LdapEndpoint.java
+++ b/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;
+ }
+ }
}
diff --git a/opendj-server-legacy/src/test/java/org/forgerock/opendj/adapter/server3x/AdaptersTestCase.java b/opendj-server-legacy/src/test/java/org/forgerock/opendj/adapter/server3x/AdaptersTestCase.java
index d48cad3..5b39ec2 100644
--- a/opendj-server-legacy/src/test/java/org/forgerock/opendj/adapter/server3x/AdaptersTestCase.java
+++ b/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);
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/protocols/http/AuthenticationFilterTest.java b/opendj-server-legacy/src/test/java/org/opends/server/protocols/http/AuthenticationFilterTest.java
deleted file mode 100644
index 9a13224..0000000
--- a/opendj-server-legacy/src/test/java/org/opends/server/protocols/http/AuthenticationFilterTest.java
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * The contents of this file are subject to the terms of the Common Development and
- * Distribution License (the License). You may not use this file except in compliance with the
- * License.
- *
- * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
- * specific language governing permission and limitations under the License.
- *
- * When distributing Covered Software, include this CDDL Header Notice in each file and include
- * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
- * Header, with the fields enclosed by brackets [] replaced by your own identifying
- * information: "Portions Copyright [year] [name of copyright owner]".
- *
- * Copyright 2013-2016 ForgeRock AS.
- */
-package org.opends.server.protocols.http;
-
-import static org.opends.server.protocols.http.AuthenticationFilter.*;
-import static org.assertj.core.api.Assertions.*;
-
-import java.io.IOException;
-
-import org.assertj.core.api.SoftAssertions;
-import org.forgerock.http.protocol.Request;
-import org.forgerock.http.protocol.Response;
-import org.forgerock.json.resource.ResourceException;
-import org.opends.server.DirectoryServerTestCase;
-import org.opends.server.util.Base64;
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.DataProvider;
-import org.testng.annotations.Test;
-
-@SuppressWarnings("javadoc")
-public class AuthenticationFilterTest extends DirectoryServerTestCase
-{
- private static final String USERNAME = "Aladdin";
- private static final String PASSWORD = "open sesame";
- private static final String BASE64_USERPASS = Base64.encode((USERNAME + ":" + PASSWORD).getBytes());
-
- private HTTPAuthenticationConfig authConfig;
- private AuthenticationFilter filter;
-
- @BeforeMethod
- private void createConfigAndFilter()
- {
- authConfig = new HTTPAuthenticationConfig();
- filter = new AuthenticationFilter(authConfig, false);
- }
-
- @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
- {
- assertThat(filter.parseUsernamePassword(authZHeader)).isNull();
- }
-
- @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
- {
- assertThat(filter.parseUsernamePassword(authZHeader)).containsExactly(USERNAME, PASSWORD);
- }
-
- @Test
- public void sendUnauthorizedResponseWithHttpBasicAuthWillChallengeUserAgent() throws Exception
- {
- authConfig.setBasicAuthenticationSupported(true);
- final Response response = sendUnauthorizedResponseWithHTTPBasicAuthChallenge();
-
- assertThat(response.getHeaders().getFirst("WWW-Authenticate")).isEqualTo("Basic realm=\"org.forgerock.opendj\"");
- verifyUnauthorizedOutputMessage(response);
- }
-
- @Test
- public void sendUnauthorizedResponseWithoutHttpBasicAuthWillNotChallengeUserAgent() throws Exception
- {
- authConfig.setBasicAuthenticationSupported(false);
- final Response response = sendUnauthorizedResponseWithHTTPBasicAuthChallenge();
-
- assertThat(response.getHeaders().getFirst("WWW-Authenticate")).isNull();
- verifyUnauthorizedOutputMessage(response);
- }
-
- private Response sendUnauthorizedResponseWithHTTPBasicAuthChallenge() throws Exception
- {
- return filter.resourceExceptionToPromise(ResourceException.getException(401, "Invalid Credentials")).get();
- }
-
- 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
- {
- authConfig.setBasicAuthenticationSupported(true);
-
- final Request request = new Request();
- request.getHeaders().add(HTTP_BASIC_AUTH_HEADER, "Basic " + BASE64_USERPASS);
- assertThat(filter.extractUsernamePassword(request)).containsExactly(USERNAME, PASSWORD);
- }
-
- @Test
- public void extractUsernamePasswordCustomHeaders() throws Exception
- {
- final String customHeaderUsername = "X-OpenIDM-Username";
- final String customHeaderPassword = "X-OpenIDM-Password";
-
- authConfig.setCustomHeadersAuthenticationSupported(true);
- authConfig.setCustomHeaderUsername(customHeaderUsername);
- authConfig.setCustomHeaderPassword(customHeaderPassword);
-
- final Request request = new Request();
- request.getHeaders().add(customHeaderUsername, USERNAME);
- request.getHeaders().add(customHeaderPassword, PASSWORD);
-
- assertThat(filter.extractUsernamePassword(request)).containsExactly(USERNAME, PASSWORD);
- }
-}
--
Gitblit v1.10.0