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

Guy Paddock
26.44.2017 2be681a57d022e204b8d66cbcf3643c57c60cc20
Adds subtree flattening to collections
6 files modified
225 ■■■■ changed files
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfigurator.java 30 ●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceCollection.java 38 ●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceImpl.java 36 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceSingleton.java 8 ●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/resources/org/forgerock/opendj/rest2ldap/rest2ldap.properties 1 ●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java 112 ●●●● patch | view | raw | blame | history
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")
        final SubResourceType subResourceType =
          config.get("type").required().as(enumConstant(SubResourceType.class));
        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)
            final SubResourceCollection collection =
                collectionOf(resourceId)
                    .urlTemplate(urlTemplate)
                                                                             .dnTemplate(dnTemplate)
                                                                             .isReadOnly(isReadOnly)
                                                                             .glueObjectClasses(glueObjectClasses);
                    .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,
        return new SubResourceImpl(
            rest2Ldap,
                                   dnFrom(context),
                                   dnTemplateString.isEmpty() ? null : glueObjectClasses,
                                   namingStrategy,
                                   resource);
            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,9 +862,20 @@
    private Rest2Ldap usersApi() throws IOException {
        return rest2Ldap(
            defaultOptions(),
            resource("api").subResource(
                collectionOf("user").dnTemplate("dc=test").useClientDnNaming("uid")),
                resource("user").objectClasses("top", "person")
            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")))