From 2be681a57d022e204b8d66cbcf3643c57c60cc20 Mon Sep 17 00:00:00 2001
From: Guy Paddock <guy@rosieapp.com>
Date: Fri, 27 Oct 2017 04:49:12 +0000
Subject: [PATCH] Adds subtree flattening to collections
---
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfigurator.java | 40 +++++--
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceCollection.java | 44 +++++++-
opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java | 164 +++++++++++++++++++++++---------
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceSingleton.java | 8 +
opendj-rest2ldap/src/main/resources/org/forgerock/opendj/rest2ldap/rest2ldap.properties | 1
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceImpl.java | 36 ++++++
6 files changed, 224 insertions(+), 69 deletions(-)
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfigurator.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfigurator.java
index 65572d5..5f83434 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfigurator.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfigurator.java
@@ -306,24 +306,37 @@
private enum NamingStrategyType { CLIENTDNNAMING, CLIENTNAMING, SERVERNAMING }
private enum SubResourceType { COLLECTION, SINGLETON }
- private static SubResource configureSubResource(final String urlTemplate, final JsonValue config) {
+ private static SubResource configureSubResource(final String urlTemplate,
+ final JsonValue config) {
final String dnTemplate = config.get("dnTemplate").defaultTo("").asString();
final Boolean isReadOnly = config.get("isReadOnly").defaultTo(false).asBoolean();
final String resourceId = config.get("resource").required().asString();
+ final Boolean flattenSubtree =
+ config.get("flattenSubtree").defaultTo(false).asBoolean();
- if (config.get("type").required().as(enumConstant(SubResourceType.class)) == SubResourceType.COLLECTION) {
- final String[] glueObjectClasses = config.get("glueObjectClasses")
- .defaultTo(emptyList())
- .asList(String.class)
- .toArray(new String[0]);
+ final SubResourceType subResourceType =
+ config.get("type").required().as(enumConstant(SubResourceType.class));
- final SubResourceCollection collection = collectionOf(resourceId).urlTemplate(urlTemplate)
- .dnTemplate(dnTemplate)
- .isReadOnly(isReadOnly)
- .glueObjectClasses(glueObjectClasses);
+ if (subResourceType == SubResourceType.COLLECTION) {
+ final String[] glueObjectClasses =
+ config.get("glueObjectClasses")
+ .defaultTo(emptyList())
+ .asList(String.class)
+ .toArray(new String[0]);
+
+ final SubResourceCollection collection =
+ collectionOf(resourceId)
+ .urlTemplate(urlTemplate)
+ .dnTemplate(dnTemplate)
+ .isReadOnly(isReadOnly)
+ .glueObjectClasses(glueObjectClasses)
+ .flattenSubtree(flattenSubtree);
final JsonValue namingStrategy = config.get("namingStrategy").required();
- switch (namingStrategy.get("type").required().as(enumConstant(NamingStrategyType.class))) {
+ final NamingStrategyType namingStrategyType =
+ namingStrategy.get("type").required().as(enumConstant(NamingStrategyType.class));
+
+ switch (namingStrategyType) {
case CLIENTDNNAMING:
collection.useClientDnNaming(namingStrategy.get("dnAttribute").required().asString());
break;
@@ -339,7 +352,10 @@
return collection;
} else {
- return singletonOf(resourceId).urlTemplate(urlTemplate).dnTemplate(dnTemplate).isReadOnly(isReadOnly);
+ return singletonOf(resourceId)
+ .urlTemplate(urlTemplate)
+ .dnTemplate(dnTemplate)
+ .isReadOnly(isReadOnly);
}
}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceCollection.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceCollection.java
index 7713ced..d66681a 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceCollection.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceCollection.java
@@ -74,9 +74,11 @@
private final Attribute glueObjectClasses = new LinkedAttribute("objectClass");
private NamingStrategy namingStrategy;
+ private boolean flattenSubtree;
SubResourceCollection(final String resourceId) {
super(resourceId);
+
useClientDnNaming("uid");
}
@@ -213,12 +215,36 @@
/**
* Indicates whether this sub-resource collection only supports read and query operations.
*
- * @param readOnly
+ * @param isReadOnly
* {@code true} if this sub-resource collection is read-only.
* @return A reference to this object.
*/
- public SubResourceCollection isReadOnly(final boolean readOnly) {
- isReadOnly = readOnly;
+ public SubResourceCollection isReadOnly(final boolean isReadOnly) {
+ this.isReadOnly = isReadOnly;
+ return this;
+ }
+
+ /**
+ * Controls whether or not LDAP entries in the hierarchy below the root entry of the resource
+ * collection are included in the list of resources (essentially, flattening the hierarchy
+ * into one collection of resources).
+ *
+ * This can only be used if the resource is read-only. The default is not to flatten, which
+ * preserves the legacy behavior of Rest2LDAP.
+ *
+ * @param flattenSubtree
+ * Whether or not to flatten the hierarchy by searching the entire subtree.
+ * @return A reference to this object.
+ * @throws IllegalArgumentException
+ * If the configuration is invalid.
+ */
+ public SubResourceCollection flattenSubtree(boolean flattenSubtree) {
+ if (flattenSubtree && !this.isReadOnly) {
+ throw new LocalizedIllegalArgumentException(
+ ERR_CONFIG_MUST_BE_READ_ONLY_TO_FLATTEN_SUBTREE.get());
+ }
+
+ this.flattenSubtree = flattenSubtree;
return this;
}
@@ -256,11 +282,13 @@
}
private SubResourceImpl collection(final Context context) {
- return new SubResourceImpl(rest2Ldap,
- dnFrom(context),
- dnTemplateString.isEmpty() ? null : glueObjectClasses,
- namingStrategy,
- resource);
+ return new SubResourceImpl(
+ rest2Ldap,
+ dnFrom(context),
+ dnTemplateString.isEmpty() ? null : glueObjectClasses,
+ namingStrategy,
+ resource,
+ this.flattenSubtree);
}
private String idFrom(final Context context) {
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceImpl.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceImpl.java
index 7cec13f..127ee64 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceImpl.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceImpl.java
@@ -31,7 +31,6 @@
import static org.forgerock.opendj.ldap.ByteString.valueOfBytes;
import static org.forgerock.opendj.ldap.Filter.alwaysFalse;
import static org.forgerock.opendj.ldap.Filter.alwaysTrue;
-import static org.forgerock.opendj.ldap.SearchScope.SINGLE_LEVEL;
import static org.forgerock.opendj.ldap.requests.Requests.*;
import static org.forgerock.opendj.rest2ldap.ReadOnUpdatePolicy.CONTROLS;
import static org.forgerock.opendj.rest2ldap.RoutingContext.newCollectionRoutingContext;
@@ -140,9 +139,11 @@
private final boolean usePermissiveModify;
private final Resource resource;
private final Attribute glueObjectClasses;
+ private final boolean flattenSubtree;
SubResourceImpl(final Rest2Ldap rest2Ldap, final DN baseDn, final Attribute glueObjectClasses,
- final NamingStrategy namingStrategy, final Resource resource) {
+ final NamingStrategy namingStrategy, final Resource resource,
+ final boolean flattenSubtree) {
this.readOnUpdatePolicy = rest2Ldap.getOptions().get(READ_ON_UPDATE_POLICY);
this.useSubtreeDelete = rest2Ldap.getOptions().get(USE_SUBTREE_DELETE);
this.usePermissiveModify = rest2Ldap.getOptions().get(USE_PERMISSIVE_MODIFY);
@@ -153,6 +154,7 @@
this.glueObjectClasses = glueObjectClasses;
this.namingStrategy = namingStrategy;
this.resource = resource;
+ this.flattenSubtree = flattenSubtree;
}
Promise<ActionResponse, ResourceException> action(
@@ -700,7 +702,7 @@
final String[] attributes = getLdapAttributesForUnknownType(request.getFields()).toArray(new String[0]);
final Filter searchFilter = ldapFilter == Filter.alwaysTrue() ? Filter.objectClassPresent()
: ldapFilter;
- final SearchRequest searchRequest = newSearchRequest(baseDn, SINGLE_LEVEL, searchFilter, attributes);
+ final SearchRequest searchRequest = createSearchRequest(searchFilter, attributes);
// Add the page results control. We can support the page offset by reading the next offset pages, or
// offset x page size resources.
@@ -1064,6 +1066,34 @@
return namingStrategy.createSearchRequest(baseDn, resourceId).addAttribute(attributes);
}
+ /**
+ * Creates a request to search LDAP for entries that match the provided search filter, and
+ * the specified attributes.
+ *
+ * If the subtree flattening is enabled, the search request will encompass the whole subtree.
+ *
+ * @param searchFilter
+ * The filter that entries must match to be returned.
+ * @param desiredAttributes
+ * The names of the attributes to be included with each entry.
+ *
+ * @return The resulting search request.
+ */
+ private SearchRequest createSearchRequest(Filter searchFilter, String[] desiredAttributes) {
+ final SearchScope searchScope;
+ final SearchRequest searchRequest;
+
+ if (SubResourceImpl.this.flattenSubtree) {
+ searchScope = SearchScope.SUBORDINATES;
+ } else {
+ searchScope = SearchScope.SINGLE_LEVEL;
+ }
+
+ searchRequest = newSearchRequest(baseDn, searchScope, searchFilter, desiredAttributes);
+
+ return searchRequest;
+ }
+
@SuppressWarnings("unused")
private static <R> AsyncFunction<LdapException, R, ResourceException> adaptLdapException(final Class<R> clazz) {
return new AsyncFunction<LdapException, R, ResourceException>() {
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceSingleton.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceSingleton.java
index 77976f2..1e8bc86 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceSingleton.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceSingleton.java
@@ -139,7 +139,13 @@
}
private SubResourceImpl singleton(final Context context) {
- return new SubResourceImpl(rest2Ldap, dnFrom(context), null, SINGLETON_NAMING_STRATEGY, resource);
+ return new SubResourceImpl(
+ rest2Ldap,
+ dnFrom(context),
+ null,
+ SINGLETON_NAMING_STRATEGY,
+ resource,
+ false);
}
/**
diff --git a/opendj-rest2ldap/src/main/resources/org/forgerock/opendj/rest2ldap/rest2ldap.properties b/opendj-rest2ldap/src/main/resources/org/forgerock/opendj/rest2ldap/rest2ldap.properties
index 16e5cf7..40631a1 100644
--- a/opendj-rest2ldap/src/main/resources/org/forgerock/opendj/rest2ldap/rest2ldap.properties
+++ b/opendj-rest2ldap/src/main/resources/org/forgerock/opendj/rest2ldap/rest2ldap.properties
@@ -149,3 +149,4 @@
ERR_PATCH_JSON_INTERNAL_PROPERTY_90=The patch request cannot be processed because it attempts to modify the \
internal field '%s' of object '%s'. This capability is not currently supported by Rest2Ldap. Applications should \
instead perform a patch which replaces the entire object '%s'
+ERR_CONFIG_MUST_BE_READ_ONLY_TO_FLATTEN_SUBTREE_91=Sub-resources must be read-only to support sub-tree flattening.
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 7b9c3ac..208023f 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
@@ -93,7 +93,7 @@
private static final QueryFilter<JsonPointer> NO_FILTER = QueryFilter.alwaysTrue();
@Test
- public void testQueryAllWithNoSubtree() throws Exception {
+ public void testQueryAllWithNoSubtreeFlattening() throws Exception {
final Connection connection = newConnection();
final List<ResourceResponse> resources = new LinkedList<>();
final QueryResponse result =
@@ -105,25 +105,76 @@
assertThat(resources).hasSize(7);
assertThat(result.getPagedResultsCookie()).isNull();
assertThat(result.getTotalPagedResults()).isEqualTo(-1);
+
+ assertThat(resources.get(0).getContent().get("_ou").isNotNull());
+ assertThat(resources.get(0).getContent().get("_ou").asString()).isEqualTo("level1");
+
+ assertThat(resources.get(1).getContent().get("_ou").isNull());
+ assertThat(resources.get(1).getId()).isEqualTo("test1");
+
+ assertThat(resources.get(2).getContent().get("_ou").isNull());
+ assertThat(resources.get(2).getId()).isEqualTo("test2");
+
+ assertThat(resources.get(3).getContent().get("_ou").isNull());
+ assertThat(resources.get(3).getId()).isEqualTo("test3");
+
+ assertThat(resources.get(4).getContent().get("_ou").isNull());
+ assertThat(resources.get(4).getId()).isEqualTo("test4");
+
+ assertThat(resources.get(5).getContent().get("_ou").isNull());
+ assertThat(resources.get(5).getId()).isEqualTo("test5");
+
+ assertThat(resources.get(6).getContent().get("_ou").isNull());
+ assertThat(resources.get(6).getId()).isEqualTo("test6");
}
-// @Test
-// public void testQueryAllWithSubtree() throws Exception {
-// final Connection connection = newConnection();
-// final List<ResourceResponse> resources = new LinkedList<>();
-// final QueryResponse result =
-// connection.query(
-// newAuthConnectionContext(),
-// newQueryRequest("").setQueryFilter(NO_FILTER),
-// resources);
-//
-// assertThat(resources).hasSize(7);
-// assertThat(result.getPagedResultsCookie()).isNull();
-// assertThat(result.getTotalPagedResults()).isEqualTo(-1);
-// }
+ @Test
+ public void testQueryAllWithSubtreeFlattening() throws Exception {
+ final Connection connection = newConnection();
+ final List<ResourceResponse> resources = new LinkedList<>();
+ final QueryResponse result =
+ connection.query(
+ newAuthConnectionContext(),
+ newQueryRequest("all-users").setQueryFilter(NO_FILTER),
+ resources);
+
+ assertThat(resources).hasSize(10);
+ assertThat(result.getPagedResultsCookie()).isNull();
+ assertThat(result.getTotalPagedResults()).isEqualTo(-1);
+
+ assertThat(resources.get(0).getContent().get("_ou").isNotNull());
+ assertThat(resources.get(0).getContent().get("_ou").asString()).isEqualTo("level1");
+
+ assertThat(resources.get(1).getContent().get("_ou").isNotNull());
+ assertThat(resources.get(1).getContent().get("_ou").asString()).isEqualTo("level2");
+
+ assertThat(resources.get(2).getContent().get("_ou").isNull());
+ assertThat(resources.get(2).getId()).isEqualTo("sub2");
+
+ assertThat(resources.get(3).getContent().get("_ou").isNull());
+ assertThat(resources.get(3).getId()).isEqualTo("sub1");
+
+ assertThat(resources.get(4).getContent().get("_ou").isNull());
+ assertThat(resources.get(4).getId()).isEqualTo("test1");
+
+ assertThat(resources.get(5).getContent().get("_ou").isNull());
+ assertThat(resources.get(5).getId()).isEqualTo("test2");
+
+ assertThat(resources.get(6).getContent().get("_ou").isNull());
+ assertThat(resources.get(6).getId()).isEqualTo("test3");
+
+ assertThat(resources.get(7).getContent().get("_ou").isNull());
+ assertThat(resources.get(7).getId()).isEqualTo("test4");
+
+ assertThat(resources.get(8).getContent().get("_ou").isNull());
+ assertThat(resources.get(8).getId()).isEqualTo("test5");
+
+ assertThat(resources.get(9).getContent().get("_ou").isNull());
+ assertThat(resources.get(9).getId()).isEqualTo("test6");
+ }
@Test
- public void testQueryNone() throws Exception {
+ public void testQueryNoneWithNoSubtreeFlattening() throws Exception {
final Connection connection = newConnection();
final List<ResourceResponse> resources = new LinkedList<>();
final QueryResponse result = connection.query(newAuthConnectionContext(),
@@ -135,6 +186,18 @@
}
@Test
+ public void testQueryNoneWithSubtreeFlattening() throws Exception {
+ final Connection connection = newConnection();
+ final List<ResourceResponse> resources = new LinkedList<>();
+ final QueryResponse result = connection.query(newAuthConnectionContext(),
+ newQueryRequest("all-users").setQueryFilter(QueryFilter.<JsonPointer> alwaysFalse()), resources);
+
+ assertThat(resources).hasSize(0);
+ assertThat(result.getPagedResultsCookie()).isNull();
+ assertThat(result.getTotalPagedResults()).isEqualTo(-1);
+ }
+
+ @Test
public void testQueryPageResultsCookie() throws Exception {
final Connection connection = newConnection();
final List<ResourceResponse> resources = new ArrayList<>();
@@ -799,35 +862,46 @@
private Rest2Ldap usersApi() throws IOException {
return rest2Ldap(
defaultOptions(),
- resource("api").subResource(
- collectionOf("user").dnTemplate("dc=test").useClientDnNaming("uid")),
- resource("user").objectClasses("top", "person")
- .property(
- "schemas",
- constant(asList("urn:scim:schemas:core:1.0")))
- .property(
- "_id",
- simple("uid").isRequired(true).writability(CREATE_ONLY))
- .property(
- "_ou",
- simple("ou").isRequired(false).writability(CREATE_ONLY))
- .property(
- "name",
- object()
- .property("displayName", simple("cn").isRequired(true))
- .property("surname", simple("sn").isRequired(true)))
- .property(
- "_rev",
- simple("etag").isRequired(true).writability(READ_ONLY))
- .property(
- "description",
- simple("description").isMultiValued(true))
- .property(
- "singleNumber",
- simple("singleNumber").decoder(byteStringToInteger()))
- .property(
- "multiNumber",
- simple("multiNumber").isMultiValued(true).decoder(byteStringToInteger()))
+ resource("api")
+ .subResource(
+ collectionOf("user")
+ .dnTemplate("dc=test")
+ .useClientDnNaming("uid"))
+ .subResource(
+ collectionOf("user")
+ .urlTemplate("all-users")
+ .dnTemplate("dc=test")
+ .useClientDnNaming("uid")
+ .isReadOnly(true)
+ .flattenSubtree(true)),
+ resource("user")
+ .objectClasses("top", "person")
+ .property(
+ "schemas",
+ constant(asList("urn:scim:schemas:core:1.0")))
+ .property(
+ "_id",
+ simple("uid").isRequired(true).writability(CREATE_ONLY))
+ .property(
+ "_ou",
+ simple("ou").isRequired(false).writability(CREATE_ONLY))
+ .property(
+ "name",
+ object()
+ .property("displayName", simple("cn").isRequired(true))
+ .property("surname", simple("sn").isRequired(true)))
+ .property(
+ "_rev",
+ simple("etag").isRequired(true).writability(READ_ONLY))
+ .property(
+ "description",
+ simple("description").isMultiValued(true))
+ .property(
+ "singleNumber",
+ simple("singleNumber").decoder(byteStringToInteger()))
+ .property(
+ "multiNumber",
+ simple("multiNumber").isMultiValued(true).decoder(byteStringToInteger()))
);
}
--
Gitblit v1.10.0