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); } } 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) { 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>() { 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); } /** 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. 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())) ); }