From 40be863aec75ae0c0c7b97ad5622ff47f476a783 Mon Sep 17 00:00:00 2001 From: Guy Elsmore-Paddock Date: Wed, 25 Oct 2017 21:46:23 -0400 Subject: [PATCH 01/15] Fix read-only sub-resource CREST API descr (#6) Corrects three obscure defects in the Rest2LDAP implementation of CREST descriptors: - Read-only sub-resources were not appearing at all in the CREST API description JSON. - When read-only sub-resources appeared in the API description alongside writable sub-resources for the same models, the generated service name for the sub-resources was the same, leading to an IllegalStateException. Now, we generate a unique service name for each sub-resource based on its writability. - A top-level `create` request was still being rendered in the API description for read-only sub-resources. --- .../rest2ldap/endpoints/api/example-v1.json | 12 ++ .../rest2ldap/ReadOnlyRequestHandler.java | 13 +++ .../forgerock/opendj/rest2ldap/Resource.java | 57 ++++++++-- .../Rest2LdapJsonConfiguratorTest.java | 104 ++++++++++++++++-- .../rest2ldap/endpoints/api/example-v1.json | 12 ++ 5 files changed, 176 insertions(+), 22 deletions(-) diff --git a/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/rest2ldap/endpoints/api/example-v1.json b/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/rest2ldap/endpoints/api/example-v1.json index 096de36f7e..b5a0b02bc3 100644 --- a/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/rest2ldap/endpoints/api/example-v1.json +++ b/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/rest2ldap/endpoints/api/example-v1.json @@ -21,6 +21,18 @@ "dnAttribute": "uid" } }, + // This resource is the same as "users", but read-only. + // Users cannot be created, modified, or deleted through this sub-resource. + "read-only-users": { + "type": "collection", + "dnTemplate": "ou=people,dc=example,dc=com", + "resource": "frapi:opendj:rest2ldap:user:1.0", + "namingStrategy": { + "type": "clientDnNaming", + "dnAttribute": "uid" + }, + "isReadOnly": true + }, "groups": { "type": "collection", "dnTemplate": "ou=groups,dc=example,dc=com", diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReadOnlyRequestHandler.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReadOnlyRequestHandler.java index 1c16427982..71435591cf 100644 --- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReadOnlyRequestHandler.java +++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReadOnlyRequestHandler.java @@ -18,6 +18,8 @@ import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_READ_ONLY_ENDPOINT; +import org.forgerock.api.models.ApiDescription; +import org.forgerock.http.ApiProducer; import org.forgerock.json.resource.BadRequestException; import org.forgerock.json.resource.QueryRequest; import org.forgerock.json.resource.QueryResourceHandler; @@ -28,6 +30,7 @@ import org.forgerock.json.resource.ResourceException; import org.forgerock.json.resource.ResourceResponse; import org.forgerock.services.context.Context; +import org.forgerock.services.descriptor.Describable; import org.forgerock.util.promise.Promise; /** @@ -56,4 +59,14 @@ public Promise handleRead( protected Promise handleRequest(final Context context, final Request request) { return new BadRequestException(ERR_READ_ONLY_ENDPOINT.get().toString()).asPromise(); } + + @Override + public ApiDescription api(ApiProducer producer) { + if (delegate instanceof Describable) { + return ((Describable)delegate).api(producer); + } + else { + return super.api(producer); + } + } } diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Resource.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Resource.java index 9c8fbd0a51..d82c741365 100644 --- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Resource.java +++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Resource.java @@ -470,6 +470,31 @@ String getResourceId() { return id; } + /** + * Gets a unique name for the configuration of this resource in CREST. + * + * The name is the combination of the resource type and the writability of the resource. For + * example, {@code frapi:opendj:rest2ldap:group:1.0:read-write} or + * {@code frapi:opendj:rest2ldap:user:1.0:read-only}. Multiple resources can share the same + * service description if they manipulate the same resource type and have the same writability. + * + * @param isReadOnly + * Whether or not this resource is read-only. + * + * @return The unique service ID for this resource, given the specified writability. + */ + String getServiceId(boolean isReadOnly) { + StringBuilder serviceId = new StringBuilder(this.getResourceId()); + + if (isReadOnly) { + serviceId.append(":read-only"); + } else { + serviceId.append(":read-write"); + } + + return serviceId.toString(); + } + void build(final Rest2Ldap rest2Ldap) { // Prevent re-entrant calls. if (isBuilt) { @@ -522,7 +547,7 @@ ApiDescription instanceApi(boolean isReadOnly) { org.forgerock.api.models.Resource.Builder resource = org.forgerock.api.models.Resource. resource() - .title(id) + .title(this.getServiceId(isReadOnly)) .description(toLS(description)) .resourceSchema(schemaRef("#/definitions/" + id)) .mvccSupported(isMvccSupported()); @@ -539,8 +564,8 @@ ApiDescription instanceApi(boolean isReadOnly) { return ApiDescription.apiDescription() .id("unused").version("unused") .definitions(definitions()) - .services(services(resource)) - .paths(paths()) + .services(services(resource, isReadOnly)) + .paths(paths(isReadOnly)) .errors(errors()) .build(); } @@ -555,13 +580,17 @@ ApiDescription instanceApi(boolean isReadOnly) { ApiDescription collectionApi(boolean isReadOnly) { org.forgerock.api.models.Resource.Builder resource = org.forgerock.api.models.Resource. resource() - .title(id) + .title(this.getServiceId(isReadOnly)) .description(toLS(description)) .resourceSchema(schemaRef("#/definitions/" + id)) .mvccSupported(isMvccSupported()); resource.items(buildItems(isReadOnly)); - resource.create(createOperation(CreateMode.ID_FROM_SERVER)); + + if (!isReadOnly) { + resource.create(createOperation(CreateMode.ID_FROM_SERVER)); + } + resource.query(Query.query() .stability(EVOLVING) .type(QueryType.FILTER) @@ -580,23 +609,29 @@ ApiDescription collectionApi(boolean isReadOnly) { return ApiDescription.apiDescription() .id("unused").version("unused") .definitions(definitions()) - .services(services(resource)) - .paths(paths()) + .services(services(resource, isReadOnly)) + .paths(paths(isReadOnly)) .errors(errors()) .build(); } - private Services services(org.forgerock.api.models.Resource.Builder resource) { + private Services services(org.forgerock.api.models.Resource.Builder resource, + boolean isReadOnly) { + final String serviceId = this.getServiceId(isReadOnly); + return Services.services() - .put(id, resource.build()) + .put(serviceId, resource.build()) .build(); } - private Paths paths() { + private Paths paths(boolean isReadOnly) { + final String serviceId = this.getServiceId(isReadOnly); + final org.forgerock.api.models.Resource resource = resourceRef("#/services/" + serviceId); + return Paths.paths() // do not put anything in the path to avoid unfortunate string concatenation // also use UNVERSIONED and rely on the router to stamp the version - .put("", versionedPath().put(UNVERSIONED, resourceRef("#/services/" + id)).build()) + .put("", versionedPath().put(UNVERSIONED, resource).build()) .build(); } diff --git a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfiguratorTest.java b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfiguratorTest.java index 2690bd4d9b..7c2e39934b 100644 --- a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfiguratorTest.java +++ b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfiguratorTest.java @@ -23,6 +23,7 @@ import static org.forgerock.util.Options.*; import java.io.File; +import java.io.IOException; import java.io.StringReader; import java.nio.file.Path; import java.nio.file.Paths; @@ -30,6 +31,9 @@ import org.forgerock.api.CrestApiProducer; import org.forgerock.api.models.ApiDescription; +import org.forgerock.api.models.Items; +import org.forgerock.api.models.Resource; +import org.forgerock.api.models.Services; import org.forgerock.http.routing.UriRouterContext; import org.forgerock.http.util.Json; import org.forgerock.json.JsonValue; @@ -53,13 +57,20 @@ public class Rest2LdapJsonConfiguratorTest extends ForgeRockTestCase { private static final String ID = "frapi:opendj:rest2ldap"; private static final String VERSION = "4.0.0"; + + private static final Path SERVLET_MODULE_PATH = + getPathToMavenModule("opendj-rest2ldap-servlet"); + private static final Path CONFIG_DIR = Paths.get( - "../opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/rest2ldap"); + SERVLET_MODULE_PATH.toString(), "src", "main", "webapp", "WEB-INF", "classes", "rest2ldap"); @Test public void testConfigureEndpointsWithApiDescription() throws Exception { - final DescribableRequestHandler handler = configureEndpoints(CONFIG_DIR.resolve("endpoints").toFile()); + final DescribableRequestHandler handler = + createDescribableHandler(CONFIG_DIR.resolve("endpoints").toFile()); + final ApiDescription api = requestApi(handler, "api/users/bjensen"); + assertThat(api).isNotNull(); // Ensure we can can pretty print and parse back the generated api description @@ -67,44 +78,115 @@ public void testConfigureEndpointsWithApiDescription() throws Exception { assertThat(api.getId()).isEqualTo(ID); assertThat(api.getVersion()).isEqualTo(VERSION); - assertThat(api.getPaths().getNames()).containsOnly("/api/users", "/api/groups"); + + assertThat(api.getPaths().getNames()).containsOnly( + "/api/users", + "/api/read-only-users", + "/api/groups"); + assertThat(api.getDefinitions().getNames()).containsOnly( "frapi:opendj:rest2ldap:object:1.0", "frapi:opendj:rest2ldap:group:1.0", "frapi:opendj:rest2ldap:user:1.0", "frapi:opendj:rest2ldap:posixUser:1.0"); + + final Services services = api.getServices(); + + assertThat(services.getNames()).containsOnly( + "frapi:opendj:rest2ldap:user:1.0:read-write", + "frapi:opendj:rest2ldap:user:1.0:read-only", + "frapi:opendj:rest2ldap:group:1.0:read-write"); + + final String[] readOnlyServices = new String[] { + "frapi:opendj:rest2ldap:user:1.0:read-only" + }; + + for (String serviceName : readOnlyServices) { + final Resource service = services.get(serviceName); + final Items items = service.getItems(); + + assertThat(service.getCreate()).isNull(); + assertThat(items.getCreate()).isNull(); + assertThat(items.getUpdate()).isNull(); + assertThat(items.getDelete()).isNull(); + assertThat(items.getPatch()).isNull(); + + assertThat(items.getRead()).isNotNull(); + } + + final String[] writableServices = new String[] { + "frapi:opendj:rest2ldap:user:1.0:read-write", + "frapi:opendj:rest2ldap:group:1.0:read-write" + }; + + for (String serviceName : writableServices) { + final Resource service = services.get(serviceName); + final Items items = service.getItems(); + + assertThat(service.getCreate()).isNotNull(); + assertThat(items.getCreate()).isNotNull(); + assertThat(items.getUpdate()).isNotNull(); + assertThat(items.getDelete()).isNotNull(); + assertThat(items.getPatch()).isNotNull(); + assertThat(items.getRead()).isNotNull(); + } + } + + private RequestHandler createRequestHandler(final File endpointsDir) throws IOException { + return Rest2LdapJsonConfigurator.configureEndpoints(endpointsDir, Options.defaultOptions()); } - private DescribableRequestHandler configureEndpoints(final File endpointsDir) throws Exception { - final RequestHandler rh = Rest2LdapJsonConfigurator.configureEndpoints(endpointsDir, Options.defaultOptions()); - DescribableRequestHandler handler = new DescribableRequestHandler(rh); + private DescribableRequestHandler createDescribableHandler(final File endpointsDir) + throws Exception { + final RequestHandler rh = createRequestHandler(endpointsDir); + final DescribableRequestHandler handler = new DescribableRequestHandler(rh); + handler.api(new CrestApiProducer(ID, VERSION)); + return handler; } - private ApiDescription requestApi(final DescribableRequestHandler handler, String uriPath) { - Context context = newRouterContext(uriPath); - Request request = newApiRequest(resourcePath(uriPath)); + private ApiDescription requestApi(final DescribableRequestHandler handler, + final String uriPath) { + final Context context = newRouterContext(uriPath); + final Request request = newApiRequest(resourcePath(uriPath)); + return handler.handleApiRequest(context, request); } private Context newRouterContext(final String uriPath) { Context ctx = new RootContext(); + ctx = new Rest2LdapContext(ctx, rest2Ldap(defaultOptions())); ctx = new UriRouterContext(ctx, null, uriPath, Collections. emptyMap()); + return ctx; } private String prettyPrint(Object o) throws Exception { final ObjectMapper objectMapper = - new ObjectMapper().registerModules(new Json.LocalizableStringModule(), new Json.JsonValueModule()); + new ObjectMapper().registerModules( + new Json.LocalizableStringModule(), + new Json.JsonValueModule()); + final ObjectWriter writer = objectMapper.writer().withDefaultPrettyPrinter(); + return writer.writeValueAsString(o); } - static JsonValue parseJson(final String json) throws Exception { + private static JsonValue parseJson(final String json) throws Exception { try (StringReader r = new StringReader(json)) { return new JsonValue(readJsonLenient(r)); } } + + private static Path getPathToClass(Class clazz) { + return Paths.get(clazz.getProtectionDomain().getCodeSource().getLocation().getPath()); + } + + private static Path getPathToMavenModule(String moduleName) { + final Path testClassPath = getPathToClass(Rest2LdapJsonConfiguratorTest.class); + + return Paths.get(testClassPath.toString(), "..", "..", "..", moduleName); + } } diff --git a/opendj-server-legacy/resource/config/rest2ldap/endpoints/api/example-v1.json b/opendj-server-legacy/resource/config/rest2ldap/endpoints/api/example-v1.json index 096de36f7e..b5a0b02bc3 100644 --- a/opendj-server-legacy/resource/config/rest2ldap/endpoints/api/example-v1.json +++ b/opendj-server-legacy/resource/config/rest2ldap/endpoints/api/example-v1.json @@ -21,6 +21,18 @@ "dnAttribute": "uid" } }, + // This resource is the same as "users", but read-only. + // Users cannot be created, modified, or deleted through this sub-resource. + "read-only-users": { + "type": "collection", + "dnTemplate": "ou=people,dc=example,dc=com", + "resource": "frapi:opendj:rest2ldap:user:1.0", + "namingStrategy": { + "type": "clientDnNaming", + "dnAttribute": "uid" + }, + "isReadOnly": true + }, "groups": { "type": "collection", "dnTemplate": "ou=groups,dc=example,dc=com", From 5e94d56381f9359a00e7e2b94ac8a9c00b39b097 Mon Sep 17 00:00:00 2001 From: Guy Elsmore-Paddock Date: Thu, 26 Oct 2017 08:22:20 -0400 Subject: [PATCH 02/15] Clarifies a JavaDoc comment --- .../src/main/java/org/forgerock/opendj/rest2ldap/Resource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Resource.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Resource.java index d82c741365..35825f6c6c 100644 --- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Resource.java +++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Resource.java @@ -471,7 +471,7 @@ String getResourceId() { } /** - * Gets a unique name for the configuration of this resource in CREST. + * Gets a unique name for the configuration of this resource as a service in CREST. * * The name is the combination of the resource type and the writability of the resource. For * example, {@code frapi:opendj:rest2ldap:group:1.0:read-write} or From 6f65305de961bd47493caf48ce59a5645bb35209 Mon Sep 17 00:00:00 2001 From: Guy Paddock Date: Tue, 24 Oct 2017 14:24:46 -0400 Subject: [PATCH 03/15] Reformat `BasicRequestsTest` test As we're going to be making a lot of changes in this area, the tests should at least be legible first. --- .../opendj/rest2ldap/BasicRequestsTest.java | 342 +++++++++++++----- 1 file changed, 256 insertions(+), 86 deletions(-) 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 343ed60138..080979ae62 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 @@ -12,6 +12,7 @@ * information: "Portions copyright [year] [name of copyright owner]". * * Copyright 2013-2016 ForgeRock AS. + * Portions Copyright 2017 Rosie Applications, Inc. */ package org.forgerock.opendj.rest2ldap; @@ -95,8 +96,12 @@ public final class BasicRequestsTest extends ForgeRockTestCase { public void testQueryAll() throws Exception { final Connection connection = newConnection(); final List resources = new LinkedList<>(); - final QueryResponse result = connection.query( - newAuthConnectionContext(), newQueryRequest("").setQueryFilter(NO_FILTER), resources); + final QueryResponse result = + connection.query( + newAuthConnectionContext(), + newQueryRequest("").setQueryFilter(NO_FILTER), + resources); + assertThat(resources).hasSize(5); assertThat(result.getPagedResultsCookie()).isNull(); assertThat(result.getTotalPagedResults()).isEqualTo(-1); @@ -108,6 +113,7 @@ public void testQueryNone() throws Exception { final List resources = new LinkedList<>(); final QueryResponse result = connection.query(newAuthConnectionContext(), newQueryRequest("").setQueryFilter(QueryFilter. alwaysFalse()), resources); + assertThat(resources).hasSize(0); assertThat(result.getPagedResultsCookie()).isNull(); assertThat(result.getTotalPagedResults()).isEqualTo(-1); @@ -119,30 +125,49 @@ public void testQueryPageResultsCookie() throws Exception { final List resources = new ArrayList<>(); // Read first page. - QueryResponse result = connection.query( - newAuthConnectionContext(), newQueryRequest("").setQueryFilter(NO_FILTER).setPageSize(2), resources); + QueryResponse result = + connection.query( + newAuthConnectionContext(), + newQueryRequest("").setQueryFilter(NO_FILTER).setPageSize(2), resources); + assertThat(result.getPagedResultsCookie()).isNotNull(); assertThat(resources).hasSize(2); assertThat(resources.get(0).getId()).isEqualTo("test1"); assertThat(resources.get(1).getId()).isEqualTo("test2"); String cookie = result.getPagedResultsCookie(); + resources.clear(); // Read second page. - result = connection.query(newAuthConnectionContext(), - newQueryRequest("").setQueryFilter(NO_FILTER).setPageSize(2).setPagedResultsCookie(cookie), resources); + result = + connection.query( + newAuthConnectionContext(), + newQueryRequest("") + .setQueryFilter(NO_FILTER) + .setPageSize(2) + .setPagedResultsCookie(cookie), + resources); + assertThat(result.getPagedResultsCookie()).isNotNull(); assertThat(resources).hasSize(2); assertThat(resources.get(0).getId()).isEqualTo("test3"); assertThat(resources.get(1).getId()).isEqualTo("test4"); cookie = result.getPagedResultsCookie(); + resources.clear(); // Read third page. - result = connection.query(newAuthConnectionContext(), - newQueryRequest("").setQueryFilter(NO_FILTER).setPageSize(2).setPagedResultsCookie(cookie), resources); + result = + connection.query( + newAuthConnectionContext(), + newQueryRequest("") + .setQueryFilter(NO_FILTER) + .setPageSize(2) + .setPagedResultsCookie(cookie), + resources); + assertThat(result.getPagedResultsCookie()).isNull(); assertThat(resources).hasSize(1); assertThat(resources.get(0).getId()).isEqualTo("test5"); @@ -152,8 +177,16 @@ public void testQueryPageResultsCookie() throws Exception { public void testQueryPageResultsIndexed() throws Exception { final Connection connection = newConnection(); final List resources = new ArrayList<>(); - QueryResponse result = connection.query(newAuthConnectionContext(), - newQueryRequest("").setQueryFilter(NO_FILTER).setPageSize(2).setPagedResultsOffset(1), resources); + + QueryResponse result = + connection.query( + newAuthConnectionContext(), + newQueryRequest("") + .setQueryFilter(NO_FILTER) + .setPageSize(2) + .setPagedResultsOffset(1), + resources); + assertThat(result.getPagedResultsCookie()).isNotNull(); assertThat(resources).hasSize(2); assertThat(resources.get(0).getId()).isEqualTo("test3"); @@ -165,6 +198,7 @@ public void testDelete() throws Exception { final Context context = newAuthConnectionContext(); final Connection connection = newConnection(); final ResourceResponse resource = connection.delete(context, newDeleteRequest("/test1")); + checkResourcesAreEqual(resource, getTestUser1(12345)); connection.read(context, newReadRequest("/test1")); } @@ -173,7 +207,9 @@ public void testDelete() throws Exception { public void testDeleteMVCCMatch() throws Exception { final Context context = newAuthConnectionContext(); final Connection connection = newConnection(); - final ResourceResponse resource = connection.delete(context, newDeleteRequest("/test1").setRevision("12345")); + final ResourceResponse resource = + connection.delete(context, newDeleteRequest("/test1").setRevision("12345")); + checkResourcesAreEqual(resource, getTestUser1(12345)); connection.read(context, newReadRequest("/test1")); } @@ -182,6 +218,7 @@ public void testDeleteMVCCMatch() throws Exception { public void testDeleteMVCCNoMatch() throws Exception { final Context context = newAuthConnectionContext(); final Connection connection = newConnection(); + connection.delete(context, newDeleteRequest("/test1").setRevision("12346")); } @@ -189,6 +226,7 @@ public void testDeleteMVCCNoMatch() throws Exception { public void testDeleteNotFound() throws Exception { final Context context = newAuthConnectionContext(); final Connection connection = newConnection(); + connection.delete(context, newDeleteRequest("/missing")); } @@ -199,6 +237,7 @@ public void testPatch() throws Exception { final ResourceResponse resource1 = connection.patch(context, newPatchRequest("/test1", add("/name/displayName", "changed"))); checkResourcesAreEqual(resource1, getTestUser1Updated(12345)); + final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1")); checkResourcesAreEqual(resource2, getTestUser1Updated(12345)); } @@ -209,6 +248,7 @@ public void testPatchEmpty() throws Exception { final Context context = newAuthConnectionContext(requests); final Connection connection = newConnection(); final ResourceResponse resource1 = connection.patch(context, newPatchRequest("/test1")); + checkResourcesAreEqual(resource1, getTestUser1(12345)); /* @@ -227,11 +267,16 @@ 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(context, - newPatchRequest("/test1", add("/description", asList("one", "two")))); + newPatchRequest( + "/test1", + add("/description", asList("one", "two")))); checkResourcesAreEqual(resource1, newContent); + final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1")); checkResourcesAreEqual(resource2, newContent); } @@ -241,26 +286,37 @@ 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( - context, newPatchRequest("/test1", add("/description/-", "one"), add("/description/-", "two"))); + + final ResourceResponse resource1 = + connection.patch( + context, + newPatchRequest("/test1", add("/description/-", "one"), + add("/description/-", "two"))); checkResourcesAreEqual(resource1, newContent); + final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1")); checkResourcesAreEqual(resource2, newContent); } @Test(expectedExceptions = BadRequestException.class) public void testPatchConstantAttribute() throws Exception { - newConnection().patch(newAuthConnectionContext(), 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(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(context, newReadRequest("/test1")); checkResourcesAreEqual(resource2, getTestUser1(12345)); } @@ -270,34 +326,52 @@ 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(context, newPatchRequest("/test1", - add("/singleNumber", 0), - add("/multiNumber", asList(100, 200)), - increment("/singleNumber", 100), - increment("/multiNumber", 100))); + 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(context, newReadRequest("/test1")); checkResourcesAreEqual(resource2, newContent); } @Test(expectedExceptions = BadRequestException.class) public void testPatchMissingRequiredAttribute() throws Exception { - newConnection().patch(newAuthConnectionContext(), newPatchRequest("/test1", remove("/name/surname"))); + newConnection().patch( + newAuthConnectionContext(), + newPatchRequest("/test1", remove("/name/surname"))); } @Test public void testPatchModifyOptionalAttribute() throws Exception { final Connection connection = newConnection(); final Context context = newAuthConnectionContext(); - connection.patch(context, newPatchRequest("/test1", add("/description", asList("one", "two")))); + + connection.patch( + context, + newPatchRequest("/test1", add("/description", asList("one", "two")))); + final ResourceResponse resource1 = - connection.patch(context, 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(context, newReadRequest("/test1")); checkResourcesAreEqual(resource2, newContent); } @@ -305,29 +379,42 @@ public void testPatchModifyOptionalAttribute() throws Exception { @Test(expectedExceptions = NotSupportedException.class) public void testPatchMultiValuedAttributeIndexAppend() throws Exception { final Connection connection = newConnection(); - connection.patch(newAuthConnectionContext(), 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(newAuthConnectionContext(), newPatchRequest("/test1", add("/description/-", - asList("one", "two")))); + + 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(newAuthConnectionContext(), 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( - context, newPatchRequest("/test1", add("/name/displayName", "changed")).setRevision("12345")); + final ResourceResponse resource1 = + connection.patch( + context, + newPatchRequest( + "/test1", add("/name/displayName", "changed")).setRevision("12345")); + checkResourcesAreEqual(resource1, getTestUser1Updated(12345)); + final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1")); checkResourcesAreEqual(resource2, getTestUser1Updated(12345)); } @@ -335,22 +422,27 @@ public void testPatchMVCCMatch() throws Exception { @Test(expectedExceptions = PreconditionFailedException.class) public void testPatchMVCCNoMatch() throws Exception { final Connection connection = newConnection(); + connection.patch( - newAuthConnectionContext(), - newPatchRequest("/test1", add("/name/displayName", "changed")).setRevision("12346")); + newAuthConnectionContext(), + newPatchRequest( + "/test1", + add("/name/displayName", "changed")).setRevision("12346")); } @Test(expectedExceptions = NotFoundException.class) public void testPatchNotFound() throws Exception { newConnection().patch( - newAuthConnectionContext(), - newPatchRequest("/missing", add("/name/displayName", "changed"))); + newAuthConnectionContext(), + newPatchRequest("/missing", add("/name/displayName", "changed"))); } @Test(expectedExceptions = BadRequestException.class) public void testPatchReadOnlyAttribute() throws Exception { // Etag is read-only. - newConnection().patch(newAuthConnectionContext(), newPatchRequest("/test1", add("_rev", "99999"))); + newConnection().patch( + newAuthConnectionContext(), + newPatchRequest("/test1", add("_rev", "99999"))); } @Test @@ -363,9 +455,15 @@ public void testPatchReplacePartialObject() throws Exception { field("_rev", "12345"), field("name", object(field("displayName", "Humpty"), field("surname", "Dumpty"))))); - final ResourceResponse resource1 = connection.patch(context, newPatchRequest("/test1", - replace("/name", object(field("displayName", "Humpty"), field("surname", "Dumpty"))))); + + 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(context, newReadRequest("/test1")); checkResourcesAreEqual(resource2, expected); } @@ -374,18 +472,23 @@ public void testPatchReplacePartialObject() throws Exception { 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"))))); - 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 JsonValue newContent = + json(object( + field("name", object(field("displayName", "Humpty"), + field("surname", "Dumpty"))))); + + 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(context, newPatchRequest("/test1", replace("/", newContent))); + connection.patch(context, newPatchRequest("/test1", replace("/", newContent))); checkResourcesAreEqual(resource1, expected); + final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1")); checkResourcesAreEqual(resource2, expected); } @@ -393,32 +496,48 @@ public void testPatchReplaceWholeObject() throws Exception { @Test(expectedExceptions = BadRequestException.class) public void testPatchSingleValuedAttributeIndexAppend() throws Exception { final Connection connection = newConnection(); - connection.patch(newAuthConnectionContext(), 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(newAuthConnectionContext(), 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(newAuthConnectionContext(), newPatchRequest("/test1", add("/name/surname", asList("black", - "white")))); + + 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(newAuthConnectionContext(), 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(newAuthConnectionContext(), newPatchRequest("/test1", add("/description/dummy", "junk"))); + + connection.patch( + newAuthConnectionContext(), + newPatchRequest("/test1", add("/description/dummy", "junk"))); } @Test(expectedExceptions = BadRequestException.class) @@ -430,7 +549,9 @@ public void testPatchUnknownSubSubAttribute() throws Exception { @Test public void testRead() throws Exception { - final ResourceResponse resource = newConnection().read(newAuthConnectionContext(), newReadRequest("/test1")); + final ResourceResponse resource = + newConnection().read(newAuthConnectionContext(), newReadRequest("/test1")); + checkResourcesAreEqual(resource, getTestUser1(12345)); } @@ -441,15 +562,19 @@ public void testReadNotFound() throws Exception { @Test public void testReadSelectAllFields() throws Exception { - final ResourceResponse resource = newConnection().read(newAuthConnectionContext(), - 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( - newAuthConnectionContext(), newReadRequest("/test1").addField("/name/surname")); + final ResourceResponse resource = + newConnection().read( + newAuthConnectionContext(), newReadRequest("/test1").addField("/name/surname")); + assertThat(resource.getId()).isEqualTo("test1"); assertThat(resource.getRevision()).isEqualTo("12345"); assertThat(resource.getContent().get("_id").asString()).isNull(); @@ -461,8 +586,10 @@ public void testReadSelectPartial() throws Exception { /** Disabled - see CREST-86 (Should JSON resource fields be case insensitive?) */ @Test(enabled = false) public void testReadSelectPartialInsensitive() throws Exception { - final ResourceResponse resource = newConnection().read( - newAuthConnectionContext(), newReadRequest("/test1").addField("/name/SURNAME")); + final ResourceResponse resource = + newConnection().read( + newAuthConnectionContext(), newReadRequest("/test1").addField("/name/SURNAME")); + assertThat(resource.getId()).isEqualTo("test1"); assertThat(resource.getRevision()).isEqualTo("12345"); assertThat(resource.getContent().get("_id").asString()).isNull(); @@ -476,8 +603,9 @@ public void testUpdate() throws Exception { final Connection connection = newConnection(); final Context context = newAuthConnectionContext(); final ResourceResponse resource1 = connection.update( - context, newUpdateRequest("/test1", getTestUser1Updated(12345))); + context, newUpdateRequest("/test1", getTestUser1Updated(12345))); checkResourcesAreEqual(resource1, getTestUser1Updated(12345)); + final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1")); checkResourcesAreEqual(resource2, getTestUser1Updated(12345)); } @@ -487,14 +615,15 @@ public void testUpdateNoChange() throws Exception { final List requests = new LinkedList<>(); final Connection connection = newConnection(); final Context context = newAuthConnectionContext(requests); - final ResourceResponse resource1 = connection.update(context, newUpdateRequest("/test1", getTestUser1(12345))); + 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). assertThat(requests).hasSize(1); assertThat(requests.get(0)).isInstanceOf(SearchRequest.class); - checkResourcesAreEqual(resource1, getTestUser1(12345)); + final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1")); checkResourcesAreEqual(resource2, getTestUser1(12345)); } @@ -504,9 +633,13 @@ 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(context, newUpdateRequest("/test1", newContent)); + + final ResourceResponse resource1 = + connection.update(context, newUpdateRequest("/test1", newContent)); checkResourcesAreEqual(resource1, newContent); + final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1")); checkResourcesAreEqual(resource2, newContent); } @@ -515,6 +648,7 @@ public void testUpdateAddOptionalAttribute() throws Exception { public void testUpdateConstantAttribute() throws Exception { final Connection connection = newConnection(); final JsonValue newContent = getTestUser1Updated(12345); + newContent.put("schemas", asList("junk")); connection.update(newAuthConnectionContext(), newUpdateRequest("/test1", newContent)); } @@ -524,11 +658,15 @@ 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(newAuthConnectionContext(), newUpdateRequest("/test1", newContent)); newContent.remove("description"); - final ResourceResponse resource1 = connection.update(context, newUpdateRequest("/test1", newContent)); + + final ResourceResponse resource1 = + connection.update(context, newUpdateRequest("/test1", newContent)); checkResourcesAreEqual(resource1, newContent); + final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1")); checkResourcesAreEqual(resource2, newContent); } @@ -537,6 +675,7 @@ public void testUpdateDeleteOptionalAttribute() throws Exception { public void testUpdateMissingRequiredAttribute() throws Exception { final Connection connection = newConnection(); final JsonValue newContent = getTestUser1Updated(12345); + newContent.get("name").remove("surname"); connection.update(newAuthConnectionContext(), newUpdateRequest("/test1", newContent)); } @@ -546,11 +685,15 @@ 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(newAuthConnectionContext(), newUpdateRequest("/test1", newContent)); newContent.put("description", asList("three")); - final ResourceResponse resource1 = connection.update(context, newUpdateRequest("/test1", newContent)); + + final ResourceResponse resource1 = + connection.update(context, newUpdateRequest("/test1", newContent)); checkResourcesAreEqual(resource1, newContent); + final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1")); checkResourcesAreEqual(resource2, newContent); } @@ -560,8 +703,11 @@ public void testUpdateMVCCMatch() throws Exception { final Connection connection = newConnection(); final Context context = newAuthConnectionContext(); final ResourceResponse resource1 = - connection.update(context, newUpdateRequest("/test1", getTestUser1Updated(12345)).setRevision("12345")); + connection.update( + context, + newUpdateRequest("/test1", getTestUser1Updated(12345)).setRevision("12345")); checkResourcesAreEqual(resource1, getTestUser1Updated(12345)); + final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1")); checkResourcesAreEqual(resource2, getTestUser1Updated(12345)); } @@ -569,27 +715,36 @@ public void testUpdateMVCCMatch() throws Exception { @Test(expectedExceptions = PreconditionFailedException.class) public void testUpdateMVCCNoMatch() throws Exception { final Connection connection = newConnection(); - connection.update(newAuthConnectionContext(), newUpdateRequest("/test1", getTestUser1Updated(12345)) - .setRevision("12346")); + + connection.update( + newAuthConnectionContext(), + newUpdateRequest("/test1", getTestUser1Updated(12345)).setRevision("12346")); } @Test(expectedExceptions = NotFoundException.class) public void testUpdateNotFound() throws Exception { final Connection connection = newConnection(); - connection.update(newAuthConnectionContext(), 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(newAuthConnectionContext(), newUpdateRequest("/test1", getTestUser1Updated(99999))); + connection.update( + newAuthConnectionContext(), + newUpdateRequest("/test1", getTestUser1Updated(99999))); } @Test(expectedExceptions = BadRequestException.class) public void testUpdateSingleValuedAttributeWithMultipleValues() throws Exception { final Connection connection = newConnection(); final JsonValue newContent = getTestUser1Updated(12345); + newContent.put("surname", asList("black", "white")); connection.update(newAuthConnectionContext(), newUpdateRequest("/test1", newContent)); } @@ -598,6 +753,7 @@ public void testUpdateSingleValuedAttributeWithMultipleValues() throws Exception public void testUpdateUnknownAttribute() throws Exception { final Connection connection = newConnection(); final JsonValue newContent = getTestUser1Updated(12345); + newContent.add("dummy", "junk"); connection.update(newAuthConnectionContext(), newUpdateRequest("/test1", newContent)); } @@ -607,26 +763,40 @@ private Connection newConnection() throws IOException { } 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("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()))); + 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( + "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())) + ); } private void checkResourcesAreEqual(final ResourceResponse actual, final JsonValue expected) { final ResourceResponse expectedResource = asResource(expected); + assertThat(actual.getId()).isEqualTo(expectedResource.getId()); assertThat(actual.getRevision()).isEqualTo(expectedResource.getRevision()); assertThat(actual.getContent().getObject()).isEqualTo(expectedResource.getContent().getObject()); From b092ba85131000627f16a15abc8203f1982f81d1 Mon Sep 17 00:00:00 2001 From: Guy Paddock Date: Tue, 24 Oct 2017 15:31:19 -0400 Subject: [PATCH 04/15] Add OUs to `BasicRequestsTest` This adds OUs to the in-memory data model being used by tests, then re-works the existing tests so they continue to pass now that we have that extra data. The next few commits will rely on these OUs to verify that subtree search is working. --- .../opendj/rest2ldap/BasicRequestsTest.java | 125 +++++++++++++++--- 1 file changed, 106 insertions(+), 19 deletions(-) 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 080979ae62..7b9c3ac997 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 @@ public final class BasicRequestsTest extends ForgeRockTestCase { private static final QueryFilter NO_FILTER = QueryFilter.alwaysTrue(); @Test - public void testQueryAll() throws Exception { + public void testQueryAllWithNoSubtree() throws Exception { final Connection connection = newConnection(); final List resources = new LinkedList<>(); final QueryResponse result = @@ -102,11 +102,26 @@ public void testQueryAll() throws Exception { newQueryRequest("").setQueryFilter(NO_FILTER), resources); - assertThat(resources).hasSize(5); + assertThat(resources).hasSize(7); assertThat(result.getPagedResultsCookie()).isNull(); assertThat(result.getTotalPagedResults()).isEqualTo(-1); } +// @Test +// public void testQueryAllWithSubtree() throws Exception { +// final Connection connection = newConnection(); +// final List 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 testQueryNone() throws Exception { final Connection connection = newConnection(); @@ -128,12 +143,22 @@ public void testQueryPageResultsCookie() throws Exception { QueryResponse result = connection.query( newAuthConnectionContext(), - newQueryRequest("").setQueryFilter(NO_FILTER).setPageSize(2), resources); + newQueryRequest("") + .setQueryFilter(NO_FILTER) + .setPageSize(3), + resources); assertThat(result.getPagedResultsCookie()).isNotNull(); - assertThat(resources).hasSize(2); - assertThat(resources.get(0).getId()).isEqualTo("test1"); - assertThat(resources.get(1).getId()).isEqualTo("test2"); + assertThat(resources).hasSize(3); + + 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"); String cookie = result.getPagedResultsCookie(); @@ -145,15 +170,22 @@ public void testQueryPageResultsCookie() throws Exception { newAuthConnectionContext(), newQueryRequest("") .setQueryFilter(NO_FILTER) - .setPageSize(2) + .setPageSize(3) .setPagedResultsCookie(cookie), resources); assertThat(result.getPagedResultsCookie()).isNotNull(); - assertThat(resources).hasSize(2); + assertThat(resources).hasSize(3); + + assertThat(resources.get(0).getContent().get("_ou").isNull()); assertThat(resources.get(0).getId()).isEqualTo("test3"); + + assertThat(resources.get(1).getContent().get("_ou").isNull()); assertThat(resources.get(1).getId()).isEqualTo("test4"); + assertThat(resources.get(2).getContent().get("_ou").isNull()); + assertThat(resources.get(2).getId()).isEqualTo("test5"); + cookie = result.getPagedResultsCookie(); resources.clear(); @@ -164,13 +196,15 @@ public void testQueryPageResultsCookie() throws Exception { newAuthConnectionContext(), newQueryRequest("") .setQueryFilter(NO_FILTER) - .setPageSize(2) + .setPageSize(3) .setPagedResultsCookie(cookie), resources); assertThat(result.getPagedResultsCookie()).isNull(); assertThat(resources).hasSize(1); - assertThat(resources.get(0).getId()).isEqualTo("test5"); + + assertThat(resources.get(0).getContent().get("_ou").isNull()); + assertThat(resources.get(0).getId()).isEqualTo("test6"); } @Test @@ -189,8 +223,8 @@ public void testQueryPageResultsIndexed() throws Exception { assertThat(result.getPagedResultsCookie()).isNotNull(); assertThat(resources).hasSize(2); - assertThat(resources.get(0).getId()).isEqualTo("test3"); - assertThat(resources.get(1).getId()).isEqualTo("test4"); + assertThat(resources.get(0).getId()).isEqualTo("test2"); + assertThat(resources.get(1).getId()).isEqualTo("test3"); } @Test(expectedExceptions = NotFoundException.class) @@ -774,6 +808,9 @@ private Rest2Ldap usersApi() throws IOException { .property( "_id", simple("uid").isRequired(true).writability(CREATE_ONLY)) + .property( + "_ou", + simple("ou").isRequired(false).writability(CREATE_ONLY)) .property( "name", object() @@ -799,18 +836,24 @@ private void checkResourcesAreEqual(final ResourceResponse actual, final JsonVal assertThat(actual.getId()).isEqualTo(expectedResource.getId()); assertThat(actual.getRevision()).isEqualTo(expectedResource.getRevision()); - assertThat(actual.getContent().getObject()).isEqualTo(expectedResource.getContent().getObject()); + + assertThat(actual.getContent().getObject()) + .isEqualTo(expectedResource.getContent().getObject()); } private AuthenticatedConnectionContext newAuthConnectionContext() throws IOException { return newAuthConnectionContext(new ArrayList()); } - private AuthenticatedConnectionContext newAuthConnectionContext(List requests) throws IOException { - return new AuthenticatedConnectionContext(ctx(), getConnectionFactory(requests).getConnection()); + private AuthenticatedConnectionContext newAuthConnectionContext(List requests) + throws IOException { + return new AuthenticatedConnectionContext( + ctx(), + getConnectionFactory(requests).getConnection()); } - private ConnectionFactory getConnectionFactory(final List requests) throws IOException { + private ConnectionFactory getConnectionFactory(final List requests) + throws IOException { // @formatter:off final MemoryBackend backend = new MemoryBackend(new LDIFEntryReader( @@ -862,7 +905,46 @@ private ConnectionFactory getConnectionFactory(final List requests) thr "userpassword: password", "cn: test user 5", "sn: user 5", - "etag: 55555" + "etag: 55555", + "", + "dn: uid=test6,dc=test", + "objectClass: top", + "objectClass: person", + "uid: test6", + "userpassword: password", + "cn: test user 6", + "sn: user 6", + "etag: 66666", + "", + "dn: ou=level1,dc=test", + "objectClass: top", + "objectClass: organizationalUnit", + "ou: level1", + "etag: 77777", + "", + "dn: uid=sub1,ou=level1,dc=test", + "objectClass: top", + "objectClass: person", + "uid: sub1", + "userpassword: password", + "cn: test user level 1", + "sn: user 7", + "etag: 88888", + "", + "dn: ou=level2,ou=level1,dc=test", + "objectClass: top", + "objectClass: organizationalUnit", + "ou: level2", + "etag: 99999", + "", + "dn: uid=sub2,ou=level2,ou=level1,dc=test", + "objectClass: top", + "objectClass: person", + "uid: sub2", + "userpassword: password", + "cn: test user level 2", + "sn: user 8", + "etag: 86753" )); // @formatter:on @@ -938,10 +1020,15 @@ public void handleModifyDN(RequestContext requestContext, ModifyDNRequest reques @Override public void handleSearch(RequestContext requestContext, SearchRequest request, - IntermediateResponseHandler intermediateResponseHandler, SearchResultHandler entryHandler, + IntermediateResponseHandler intermediateResponseHandler, + SearchResultHandler entryHandler, LdapResultHandler resultHandler) { requests.add(request); - handler.handleSearch(requestContext, request, intermediateResponseHandler, entryHandler, + handler.handleSearch( + requestContext, + request, + intermediateResponseHandler, + entryHandler, resultHandler); } From a9343b0653694fa55fc5d58f8ca6798f5cc982de Mon Sep 17 00:00:00 2001 From: Guy Paddock Date: Tue, 24 Oct 2017 15:58:58 -0400 Subject: [PATCH 05/15] Correct FR's spelling of "CONFIG" --- .../opendj/rest2ldap/Rest2LdapHttpApplication.java | 7 ++++--- .../org/forgerock/opendj/rest2ldap/rest2ldap.properties | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) 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 66e0a5c688..5efd9569df 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 @@ -12,6 +12,7 @@ * information: "Portions copyright [year] [name of copyright owner]". * * Copyright 2015-2016 ForgeRock AS. + * Portions Copyright 2017 Rosie Applications, Inc. */ package org.forgerock.opendj.rest2ldap; @@ -371,7 +372,7 @@ private AccessTokenResolver parseRfc7662Resolver(final JsonValue configuration) rfc7662.get("clientId").required().asString(), rfc7662.get("clientSecret").required().asString()); } catch (final URISyntaxException e) { - throw new IllegalArgumentException(ERR_CONIFG_OAUTH2_INVALID_INTROSPECT_URL.get( + throw new IllegalArgumentException(ERR_CONFIG_OAUTH2_INVALID_INTROSPECT_URL.get( introspectionEndPointURL, e.getLocalizedMessage()).toString(), e); } } @@ -394,8 +395,8 @@ private Duration parseCacheExpiration(final JsonValue expirationJson) { final Duration expiration = expirationJson.as(duration()); if (expiration.isZero() || expiration.isUnlimited()) { throw newJsonValueException(expirationJson, - expiration.isZero() ? ERR_CONIFG_OAUTH2_CACHE_ZERO_DURATION.get() - : ERR_CONIFG_OAUTH2_CACHE_UNLIMITED_DURATION.get()); + expiration.isZero() ? ERR_CONFIG_OAUTH2_CACHE_ZERO_DURATION.get() + : ERR_CONFIG_OAUTH2_CACHE_UNLIMITED_DURATION.get()); } return expiration; } catch (final Exception e) { 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 3c1e587208..16e5cf74b7 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 @@ -12,14 +12,15 @@ # information: "Portions Copyright [year] [name of copyright owner]". # # Copyright 2016 ForgeRock AS. +# Portions Copyright 2017 Rosie Applications, Inc. # # Configuration errors ERR_FAIL_PARSE_CONFIGURATION_1=Unable to start Rest2Ldap Http Application due to the configuration error: '%s' ERR_CONFIG_OAUTH2_UNSUPPORTED_ACCESS_TOKEN_RESOLVER_2= '%s'is not a supported access token resolver. Must be one of '%s' -ERR_CONIFG_OAUTH2_INVALID_INTROSPECT_URL_3=The token introspection endpoint '%s' is not a valid URL: '%s' -ERR_CONIFG_OAUTH2_CACHE_ZERO_DURATION_4=The cache expiration duration cannot be zero -ERR_CONIFG_OAUTH2_CACHE_UNLIMITED_DURATION_5=The cache expiration duration cannot be unlimited +ERR_CONFIG_OAUTH2_INVALID_INTROSPECT_URL_3=The token introspection endpoint '%s' is not a valid URL: '%s' +ERR_CONFIG_OAUTH2_CACHE_ZERO_DURATION_4=The cache expiration duration cannot be zero +ERR_CONFIG_OAUTH2_CACHE_UNLIMITED_DURATION_5=The cache expiration duration cannot be unlimited ERR_CONFIG_OAUTH2_CACHE_INVALID_DURATION_6=Malformed duration value '%s' for cache expiration. \ The duration syntax supports all human readable notations from day ('days'', 'day'', 'd'') to nanosecond \ ('nanoseconds', 'nanosecond', 'nanosec', 'nanos', 'nano', 'ns') From ce283e9af9ec1c0cdc76e4fe7b3ec2b7d8973c75 Mon Sep 17 00:00:00 2001 From: Guy Paddock Date: Wed, 25 Oct 2017 21:53:57 -0400 Subject: [PATCH 06/15] Fixes FR's spelling of "ignored" --- .../opendj/rest2ldap/OAuth2JsonConfigurationTestCase.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/OAuth2JsonConfigurationTestCase.java b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/OAuth2JsonConfigurationTestCase.java index 98b08f0b24..7c10cae42d 100644 --- a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/OAuth2JsonConfigurationTestCase.java +++ b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/OAuth2JsonConfigurationTestCase.java @@ -190,7 +190,7 @@ public void testInvalidCacheResolverConfigurations(final String rawJson) throws } @DataProvider - public Object[][] ingnoredCacheResolverConfigurations() { + public Object[][] ignoredCacheResolverConfigurations() { // @Checkstyle:off return new Object[][] { { @@ -205,7 +205,7 @@ public Object[][] ingnoredCacheResolverConfigurations() { // @Checkstyle:on } - @Test(dataProvider = "ingnoredCacheResolverConfigurations") + @Test(dataProvider = "ignoredCacheResolverConfigurations") public void testNoCacheFallbackOnResolver(final String rawJson) throws Exception { assertThat(fakeApp.createCachedTokenResolverIfNeeded(parseJson(rawJson), resolver)).isEqualTo(resolver); } From 2be681a57d022e204b8d66cbcf3643c57c60cc20 Mon Sep 17 00:00:00 2001 From: Guy Paddock Date: Thu, 26 Oct 2017 09:44:11 -0400 Subject: [PATCH 07/15] Adds subtree flattening to collections --- .../rest2ldap/Rest2LdapJsonConfigurator.java | 44 +++-- .../rest2ldap/SubResourceCollection.java | 44 ++++- .../opendj/rest2ldap/SubResourceImpl.java | 36 +++- .../rest2ldap/SubResourceSingleton.java | 8 +- .../opendj/rest2ldap/rest2ldap.properties | 1 + .../opendj/rest2ldap/BasicRequestsTest.java | 164 +++++++++++++----- 6 files changed, 226 insertions(+), 71 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 65572d58ab..5f83434369 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 static Resource configureResource(final String resourceId, final JsonVal 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(); - - 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 SubResourceCollection collection = collectionOf(resourceId).urlTemplate(urlTemplate) - .dnTemplate(dnTemplate) - .isReadOnly(isReadOnly) - .glueObjectClasses(glueObjectClasses); + final Boolean flattenSubtree = + config.get("flattenSubtree").defaultTo(false).asBoolean(); + + 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) + .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 @@ private static SubResource configureSubResource(final String urlTemplate, final 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 7713ced9e7..d66681a2fc 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 @@ public final class SubResourceCollection extends SubResource { 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 @@ public SubResourceCollection glueObjectClasses(final String... objectClasses) { /** * 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 @@ public Promise apply(LdapException e) } 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 7cec13fe39..127ee64cbf 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 @@ final class SubResourceImpl { 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 @@ final class SubResourceImpl { this.glueObjectClasses = glueObjectClasses; this.namingStrategy = namingStrategy; this.resource = resource; + this.flattenSubtree = flattenSubtree; } Promise action( @@ -700,7 +702,7 @@ public Promise apply(final Filter ldapFilter) 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 @@ private SearchRequest searchRequestForUnknownType(final String resourceId, final 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 AsyncFunction adaptLdapException(final Class clazz) { return new AsyncFunction() { 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 77976f242b..1e8bc865c9 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 @@ Promise route(final Context context) { } 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 16e5cf74b7..40631a12e9 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_JSON_QUERY_PARSE_ERROR_89=The value '%s' could not be parsed as a valid JSON 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 7b9c3ac997..208023f09e 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 @@ public final class BasicRequestsTest extends ForgeRockTestCase { private static final QueryFilter NO_FILTER = QueryFilter.alwaysTrue(); @Test - public void testQueryAllWithNoSubtree() throws Exception { + public void testQueryAllWithNoSubtreeFlattening() throws Exception { final Connection connection = newConnection(); final List resources = new LinkedList<>(); final QueryResponse result = @@ -105,25 +105,76 @@ public void testQueryAllWithNoSubtree() throws Exception { 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 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 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 resources = new LinkedList<>(); final QueryResponse result = connection.query(newAuthConnectionContext(), @@ -134,6 +185,18 @@ public void testQueryNone() throws Exception { assertThat(result.getTotalPagedResults()).isEqualTo(-1); } + @Test + public void testQueryNoneWithSubtreeFlattening() throws Exception { + final Connection connection = newConnection(); + final List resources = new LinkedList<>(); + final QueryResponse result = connection.query(newAuthConnectionContext(), + newQueryRequest("all-users").setQueryFilter(QueryFilter. 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(); @@ -799,35 +862,46 @@ private Connection newConnection() throws IOException { 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())) ); } From 114516cdeb8c20f90a3c0d884effb5b145c4079b Mon Sep 17 00:00:00 2001 From: Guy Paddock Date: Thu, 26 Oct 2017 20:46:15 -0400 Subject: [PATCH 08/15] Stop warnings about safe typecast --- .../org/forgerock/opendj/rest2ldap/ReadOnlyRequestHandler.java | 1 + 1 file changed, 1 insertion(+) diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReadOnlyRequestHandler.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReadOnlyRequestHandler.java index 71435591cf..f962a10eac 100644 --- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReadOnlyRequestHandler.java +++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReadOnlyRequestHandler.java @@ -61,6 +61,7 @@ protected Promise handleRequest(final Context context, } @Override + @SuppressWarnings("unchecked") public ApiDescription api(ApiProducer producer) { if (delegate instanceof Describable) { return ((Describable)delegate).api(producer); From a2a71e1777beb48f98e64811ca6cd43286b79be3 Mon Sep 17 00:00:00 2001 From: Guy Paddock Date: Thu, 26 Oct 2017 20:52:53 -0400 Subject: [PATCH 09/15] Tests for sub-tree flattening config Also adds a few accessors in order to faciliate accessors for tests. More work is needed in this area -- FR's code is an inconsistent mess of final classes, fluent factory methods, and accessors with the wrong access level. --- .../rest2ldap/endpoints/api/example-v1.json | 14 ++ .../forgerock/opendj/rest2ldap/Resource.java | 19 +- .../opendj/rest2ldap/SubResource.java | 25 ++- .../rest2ldap/SubResourceCollection.java | 11 + .../Rest2LdapJsonConfiguratorTest.java | 204 +++++++++++++++++- .../rest2ldap/endpoints/api/example-v1.json | 14 ++ 6 files changed, 280 insertions(+), 7 deletions(-) diff --git a/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/rest2ldap/endpoints/api/example-v1.json b/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/rest2ldap/endpoints/api/example-v1.json index b5a0b02bc3..90de7e3c99 100644 --- a/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/rest2ldap/endpoints/api/example-v1.json +++ b/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/rest2ldap/endpoints/api/example-v1.json @@ -33,6 +33,20 @@ }, "isReadOnly": true }, + // This resource provides a read-only view of all users in the system, including + // users nested underneath entries like org units, organizations, etc., starting + // from "ou=people,dc=example,dc=com" and working down. + "all-users": { + "type": "collection", + "dnTemplate": "ou=people,dc=example,dc=com", + "resource": "frapi:opendj:rest2ldap:user:1.0", + "namingStrategy": { + "type": "clientDnNaming", + "dnAttribute": "uid" + }, + "isReadOnly": true, + "flattenSubtree": true + }, "groups": { "type": "collection", "dnTemplate": "ou=groups,dc=example,dc=com", diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Resource.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Resource.java index 35825f6c6c..e7964bd7d6 100644 --- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Resource.java +++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Resource.java @@ -34,6 +34,8 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -484,7 +486,7 @@ String getResourceId() { * @return The unique service ID for this resource, given the specified writability. */ String getServiceId(boolean isReadOnly) { - StringBuilder serviceId = new StringBuilder(this.getResourceId()); + final StringBuilder serviceId = new StringBuilder(this.getResourceId()); if (isReadOnly) { serviceId.append(":read-only"); @@ -495,6 +497,21 @@ String getServiceId(boolean isReadOnly) { return serviceId.toString(); } + /** + * Gets a map of the sub-resources under this resource, keyed by URL template. + * + * @return The map of sub-resource URL templates to sub-resources. + */ + Map getSubResourceMap() { + final Map result = new HashMap<>(); + + for (SubResource subResource : this.subResources) { + result.put(subResource.getUrlTemplate(), subResource); + } + + return result; + } + void build(final Rest2Ldap rest2Ldap) { // Prevent re-entrant calls. if (isBuilt) { diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResource.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResource.java index 0e61043650..ee6a16af36 100644 --- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResource.java +++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResource.java @@ -60,9 +60,10 @@ public abstract class SubResource { String urlTemplate = ""; String dnTemplateString = ""; - boolean isReadOnly = false; - Rest2Ldap rest2Ldap; - Resource resource; + + protected boolean isReadOnly = false; + protected Rest2Ldap rest2Ldap; + protected Resource resource; SubResource(final String resourceId) { this.resourceId = resourceId; @@ -80,9 +81,27 @@ public final int hashCode() { @Override public final String toString() { + return getUrlTemplate(); + } + + /** + * Gets the URL template that must match for this sub-resource to apply to a given request. + * + * @return The URL template for this sub-resource. + */ + public String getUrlTemplate() { return urlTemplate; } + /** + * Gets whether or not this sub-resource has been configured for read-only access. + * + * @return {@code true} if the sub-resource is read-only; {@code false} otherwise. + */ + public boolean isReadOnly() { + return isReadOnly; + } + final Resource getResource() { return resource; } 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 d66681a2fc..85fd139990 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 @@ -82,6 +82,17 @@ public final class SubResourceCollection extends SubResource { useClientDnNaming("uid"); } + /** + * Gets whether or not this sub-resource should flatten sub-entries in results. + * + * @return {@code true} if entries deep in the sub-tree are included in a flattened + * collection view; {@code false} if only entries at the top level of the DN for this + * sub-resource should be returned. + */ + public boolean shouldFlattenSubtree() { + return flattenSubtree; + } + /** * Indicates that the JSON resource ID must be provided by the user, and will be used for naming the associated LDAP * entry. More specifically, LDAP entry names will be derived by appending a single RDN to the collection's base DN diff --git a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfiguratorTest.java b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfiguratorTest.java index 7c2e39934b..2a74a2d4a7 100644 --- a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfiguratorTest.java +++ b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfiguratorTest.java @@ -29,6 +29,8 @@ import java.nio.file.Paths; import java.util.Collections; +import java.util.List; +import java.util.Map; import org.forgerock.api.CrestApiProducer; import org.forgerock.api.models.ApiDescription; import org.forgerock.api.models.Items; @@ -43,6 +45,7 @@ import org.forgerock.services.context.RootContext; import org.forgerock.testng.ForgeRockTestCase; import org.forgerock.util.Options; +import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import com.fasterxml.jackson.databind.ObjectMapper; @@ -66,9 +69,8 @@ public class Rest2LdapJsonConfiguratorTest extends ForgeRockTestCase { @Test public void testConfigureEndpointsWithApiDescription() throws Exception { - final DescribableRequestHandler handler = - createDescribableHandler(CONFIG_DIR.resolve("endpoints").toFile()); - + final File endpointsDir = CONFIG_DIR.resolve("endpoints").toFile(); + final DescribableRequestHandler handler = createDescribableHandler(endpointsDir); final ApiDescription api = requestApi(handler, "api/users/bjensen"); assertThat(api).isNotNull(); @@ -82,6 +84,7 @@ public void testConfigureEndpointsWithApiDescription() throws Exception { assertThat(api.getPaths().getNames()).containsOnly( "/api/users", "/api/read-only-users", + "/api/all-users", "/api/groups"); assertThat(api.getDefinitions().getNames()).containsOnly( @@ -132,6 +135,201 @@ public void testConfigureEndpointsWithApiDescription() throws Exception { } } + @DataProvider + public Object[][] invalidSubResourceConfigurations() { + // @Checkstyle:off + return new Object[][] { + { + "{" + + "'example-v1': {" + + "'subResources': {" + + "'writeable-collection': {" + + "'type': 'collection'," + + "'dnTemplate': 'ou=people,dc=example,dc=com'," + + "'resource': 'frapi:opendj:rest2ldap:user:1.0'," + + "'namingStrategy': {" + + "'type': 'clientDnNaming'," + + "'dnAttribute': 'uid'" + + "}," + + "'flattenSubtree': true" + + "}" + + "}" + + "}" + + "}" + }, + { + "{" + + "'example-v1': {" + + "'subResources': {" + + "'writeable-collection': {" + + "'type': 'collection'," + + "'dnTemplate': 'ou=people,dc=example,dc=com'," + + "'resource': 'frapi:opendj:rest2ldap:user:1.0'," + + "'namingStrategy': {" + + "'type': 'clientDnNaming'," + + "'dnAttribute': 'uid'" + + "}," + + "'isReadOnly': false," + + "'flattenSubtree': true" + + "}" + + "}" + + "}" + + "}" + } + }; + // @Checkstyle:on + } + + @DataProvider + public Object[][] validSubResourceConfigurations() { + // @Checkstyle:off + return new Object[][] { + { + false, + false, + "{" + + "'example-v1': {" + + "'subResources': {" + + "'all-users': {" + + "'type': 'collection'," + + "'dnTemplate': 'ou=people,dc=example,dc=com'," + + "'resource': 'frapi:opendj:rest2ldap:user:1.0'," + + "'namingStrategy': {" + + "'type': 'clientDnNaming'," + + "'dnAttribute': 'uid'" + + "}," + + "'flattenSubtree': false" + + "}" + + "}" + + "}" + + "}" + }, + { + true, + false, + "{" + + "'example-v1': {" + + "'subResources': {" + + "'all-users': {" + + "'type': 'collection'," + + "'dnTemplate': 'ou=people,dc=example,dc=com'," + + "'resource': 'frapi:opendj:rest2ldap:user:1.0'," + + "'namingStrategy': {" + + "'type': 'clientDnNaming'," + + "'dnAttribute': 'uid'" + + "}," + + "'isReadOnly': true" + + "}" + + "}" + + "}" + + "}" + }, + { + true, + false, + "{" + + "'example-v1': {" + + "'subResources': {" + + "'all-users': {" + + "'type': 'collection'," + + "'dnTemplate': 'ou=people,dc=example,dc=com'," + + "'resource': 'frapi:opendj:rest2ldap:user:1.0'," + + "'namingStrategy': {" + + "'type': 'clientDnNaming'," + + "'dnAttribute': 'uid'" + + "}," + + "'isReadOnly': true," + + "'flattenSubtree': false" + + "}" + + "}" + + "}" + + "}" + }, + { + false, + false, + "{" + + "'example-v1': {" + + "'subResources': {" + + "'all-users': {" + + "'type': 'collection'," + + "'dnTemplate': 'ou=people,dc=example,dc=com'," + + "'resource': 'frapi:opendj:rest2ldap:user:1.0'," + + "'namingStrategy': {" + + "'type': 'clientDnNaming'," + + "'dnAttribute': 'uid'" + + "}," + + "'isReadOnly': false," + + "'flattenSubtree': false" + + "}" + + "}" + + "}" + + "}" + }, + { + true, + true, + "{" + + "'example-v1': {" + + "'subResources': {" + + "'all-users': {" + + "'type': 'collection'," + + "'dnTemplate': 'ou=people,dc=example,dc=com'," + + "'resource': 'frapi:opendj:rest2ldap:user:1.0'," + + "'namingStrategy': {" + + "'type': 'clientDnNaming'," + + "'dnAttribute': 'uid'" + + "}," + + "'isReadOnly': true," + + "'flattenSubtree': true" + + "}" + + "}" + + "}" + + "}" + } + }; + // @Checkstyle:on + } + + @Test(dataProvider = "invalidSubResourceConfigurations") + public void testInvalidSubResourceConfigurations(final String rawJson) throws Exception { + try { + Rest2LdapJsonConfigurator.configureResources(parseJson(rawJson)); + + fail("Expected an IllegalArgumentException"); + } + catch (IllegalArgumentException ex) { + assertThat(ex.getMessage()) + .isEqualTo("Sub-resources must be read-only to support sub-tree flattening."); + } + } + + @Test(dataProvider = "validSubResourceConfigurations") + public void testValidSubResourceConfigurations(final boolean expectingReadOnly, + final boolean expectingSubtreeFlattened, + final String rawJson) throws Exception { + final List resources = + Rest2LdapJsonConfigurator.configureResources(parseJson(rawJson)); + final org.forgerock.opendj.rest2ldap.Resource firstResource; + final Map subResources; + final SubResourceCollection allUsersSubResource; + + assertThat(resources.size()).isEqualTo(1); + + firstResource = resources.get(0); + + assertThat(firstResource.getResourceId()).isEqualTo("example-v1"); + + subResources = firstResource.getSubResourceMap(); + + assertThat(subResources.size()).isEqualTo(1); + + allUsersSubResource = (SubResourceCollection)subResources.get("all-users"); + + assertThat(allUsersSubResource.isReadOnly()).isEqualTo(expectingReadOnly); + assertThat(allUsersSubResource.shouldFlattenSubtree()).isEqualTo(expectingSubtreeFlattened); + } + private RequestHandler createRequestHandler(final File endpointsDir) throws IOException { return Rest2LdapJsonConfigurator.configureEndpoints(endpointsDir, Options.defaultOptions()); } diff --git a/opendj-server-legacy/resource/config/rest2ldap/endpoints/api/example-v1.json b/opendj-server-legacy/resource/config/rest2ldap/endpoints/api/example-v1.json index b5a0b02bc3..90de7e3c99 100644 --- a/opendj-server-legacy/resource/config/rest2ldap/endpoints/api/example-v1.json +++ b/opendj-server-legacy/resource/config/rest2ldap/endpoints/api/example-v1.json @@ -33,6 +33,20 @@ }, "isReadOnly": true }, + // This resource provides a read-only view of all users in the system, including + // users nested underneath entries like org units, organizations, etc., starting + // from "ou=people,dc=example,dc=com" and working down. + "all-users": { + "type": "collection", + "dnTemplate": "ou=people,dc=example,dc=com", + "resource": "frapi:opendj:rest2ldap:user:1.0", + "namingStrategy": { + "type": "clientDnNaming", + "dnAttribute": "uid" + }, + "isReadOnly": true, + "flattenSubtree": true + }, "groups": { "type": "collection", "dnTemplate": "ou=groups,dc=example,dc=com", From 5060dad8106471069963df5b75ada0ceadefcb97 Mon Sep 17 00:00:00 2001 From: Guy Paddock Date: Thu, 26 Oct 2017 23:20:20 -0400 Subject: [PATCH 10/15] Minor clean-up of ref property mapper Minor tweaks to Javadocs and a little formatting (this class needs a lot more). --- .../rest2ldap/ReferencePropertyMapper.java | 41 +++++++++---------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferencePropertyMapper.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferencePropertyMapper.java index 98796812e5..2ceacad654 100644 --- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferencePropertyMapper.java +++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferencePropertyMapper.java @@ -82,6 +82,7 @@ public final class ReferencePropertyMapper extends AbstractLdapPropertyMapper getLdapFilter(final Context context, final Re return mapper.getLdapFilter(context, resource, path, subPath, type, operator, valueAssertion) .thenAsync(new AsyncFunction() { @Override - public Promise apply(final Filter result) { + public Promise apply(final Filter filter) { // Search for all referenced entries and construct a filter. - final SearchRequest request = createSearchRequest(context, result); + final SearchRequest request = createSearchRequest(context, filter); final List subFilters = new LinkedList<>(); return connectionFrom(context).searchAsync(request, new SearchResultHandler() { @@ -325,8 +323,9 @@ public JsonValue apply(final List value) { } } - private SearchRequest createSearchRequest(final Context context, final Filter result) { - final Filter searchFilter = filter != null ? Filter.and(filter, result) : result; + private SearchRequest createSearchRequest(final Context context, final Filter filter) { + final Filter searchFilter = this.filter != null ? Filter.and(this.filter, filter) : filter; + return newSearchRequest(baseDnTemplate.format(context), scope, searchFilter, "1.1"); } From fad88bae0655787d9030d4f313c0a0dfcf2e47bb Mon Sep 17 00:00:00 2001 From: Guy Paddock Date: Thu, 26 Oct 2017 23:33:56 -0400 Subject: [PATCH 11/15] Sub-resource search filter support Adds the internals needed to apply search filters to collection sub-resources. --- .../rest2ldap/SubResourceCollection.java | 53 ++- .../opendj/rest2ldap/SubResourceImpl.java | 39 ++- .../rest2ldap/SubResourceSingleton.java | 7 +- .../opendj/rest2ldap/BasicRequestsTest.java | 308 ++++++++++++++---- 4 files changed, 333 insertions(+), 74 deletions(-) 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 85fd139990..05b27200a3 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 @@ -15,6 +15,7 @@ */ package org.forgerock.opendj.rest2ldap; +import static org.forgerock.guava.common.base.Preconditions.checkNotNull; import static org.forgerock.http.routing.RoutingMode.EQUALS; import static org.forgerock.http.routing.RoutingMode.STARTS_WITH; import static org.forgerock.json.resource.RouteMatchers.requestUriMatcher; @@ -75,6 +76,7 @@ public final class SubResourceCollection extends SubResource { private NamingStrategy namingStrategy; private boolean flattenSubtree; + private Filter baseSearchFilter; SubResourceCollection(final String resourceId) { super(resourceId); @@ -93,6 +95,18 @@ public boolean shouldFlattenSubtree() { return flattenSubtree; } + /** + * Gets the base filter that always restricts what LDAP entries are accessible through this + * collection, before any filters are applied from the request itself. + * + * The default is {@code null} (no base filter restriction at all). + * + * @return Either a search filter; or {@code null} if no base search filter has been defined. + */ + public Filter getBaseSearchFilter() { + return baseSearchFilter; + } + /** * Indicates that the JSON resource ID must be provided by the user, and will be used for naming the associated LDAP * entry. More specifically, LDAP entry names will be derived by appending a single RDN to the collection's base DN @@ -259,6 +273,42 @@ public SubResourceCollection flattenSubtree(boolean flattenSubtree) { return this; } + /** + * Sets the base filter that always restricts what LDAP entries are accessible through this + * collection, before any filters are applied from the request itself. + * + * The default is {@code null} (no base filter restriction at all). + * + * @param filter + * The filter which should be used to restrict which LDAP entries are returned. + * @return A reference to this object. + */ + public SubResourceCollection baseSearchFilter(final Filter filter) { + this.baseSearchFilter = filter; + return this; + } + + /** + * Sets the base filter that always restricts what LDAP entries are accessible through this + * collection, before any filters are applied from the request itself. + * + * The default is {@code null} (no base filter restriction at all). + * + * @param filter + * The filter which should be used to restrict which LDAP entries are returned. + * @return A reference to this object. + */ + public SubResourceCollection baseSearchFilter(final String filter) { + if (filter == null) { + baseSearchFilter((Filter)null); + } + else { + baseSearchFilter(Filter.valueOf(filter)); + } + + return this; + } + @Override Router addRoutes(final Router router) { router.addRoute(requestUriMatcher(EQUALS, urlTemplate), readOnly(new CollectionHandler())); @@ -299,7 +349,8 @@ private SubResourceImpl collection(final Context context) { dnTemplateString.isEmpty() ? null : glueObjectClasses, namingStrategy, resource, - this.flattenSubtree); + flattenSubtree, + baseSearchFilter); } 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 127ee64cbf..4b65ea9a35 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 @@ -140,10 +140,16 @@ final class SubResourceImpl { private final Resource resource; private final Attribute glueObjectClasses; private final boolean flattenSubtree; + private Filter baseSearchFilter; + + SubResourceImpl(final Rest2Ldap rest2Ldap, final DN baseDn, final Attribute glueObjectClasses, + final NamingStrategy namingStrategy, final Resource resource) { + this(rest2Ldap, baseDn, glueObjectClasses, namingStrategy, resource, false, null); + } SubResourceImpl(final Rest2Ldap rest2Ldap, final DN baseDn, final Attribute glueObjectClasses, final NamingStrategy namingStrategy, final Resource resource, - final boolean flattenSubtree) { + final boolean flattenSubtree, final Filter baseSearchFilter) { 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); @@ -155,6 +161,7 @@ final class SubResourceImpl { this.namingStrategy = namingStrategy; this.resource = resource; this.flattenSubtree = flattenSubtree; + this.baseSearchFilter = baseSearchFilter; } Promise action( @@ -506,9 +513,34 @@ public Promise apply(Entry entry) throws Re Promise query( final Context context, final QueryRequest request, final QueryResourceHandler resourceHandler) { return getLdapFilter(context, request.getQueryFilter()) + .then(applyBaseSearchFilter()) .thenAsync(runQuery(context, request, resourceHandler)); } + /** + * Generates a function that applies any base filter that this sub-resource may have been + * initialized with. + * + * @return The function to invoke to apply a base filter, if one has been specified. + */ + private Function applyBaseSearchFilter() { + return new Function() { + @Override + public Filter apply(final Filter requestFilter) throws ResourceException { + final Filter baseSearchFilter = SubResourceImpl.this.baseSearchFilter, + searchFilter; + + if (baseSearchFilter != null) { + searchFilter = Filter.and(baseSearchFilter, requestFilter); + } else { + searchFilter = requestFilter; + } + + return searchFilter; + } + }; + } + // FIXME: supporting assertions against sub-type properties. private Promise getLdapFilter( final Context context, final QueryFilter queryFilter) { @@ -525,6 +557,7 @@ private Promise getLdapFilter( public Promise visitAndFilter( final Void unused, final List> subFilters) { final List> promises = new ArrayList<>(subFilters.size()); + for (final QueryFilter subFilter : subFilters) { promises.add(subFilter.accept(this, unused)); } @@ -534,14 +567,17 @@ public Promise visitAndFilter( public Filter apply(final List value) { // Check for unmapped filter components and optimize. final Iterator i = value.iterator(); + while (i.hasNext()) { final Filter f = i.next(); + if (f == alwaysFalse()) { return alwaysFalse(); } else if (f == alwaysTrue()) { i.remove(); } } + switch (value.size()) { case 0: return alwaysTrue(); @@ -675,6 +711,7 @@ public Promise visitStartsWithFilter( parentDnAndType, resource, ROOT, field, STARTS_WITH, null, valueAssertion); } }; + // Note that the returned LDAP filter may be null if it could not be mapped by any property mappers. return queryFilter.accept(visitor, null); } 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 1e8bc865c9..92a1f338d9 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 @@ -140,12 +140,7 @@ Promise route(final Context context) { private SubResourceImpl singleton(final Context context) { return new SubResourceImpl( - rest2Ldap, - dnFrom(context), - null, - SINGLETON_NAMING_STRATEGY, - resource, - false); + rest2Ldap, dnFrom(context), null, SINGLETON_NAMING_STRATEGY, resource); } /** 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 208023f09e..cb1bab0c8d 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 @@ public final class BasicRequestsTest extends ForgeRockTestCase { private static final QueryFilter NO_FILTER = QueryFilter.alwaysTrue(); @Test - public void testQueryAllWithNoSubtreeFlattening() throws Exception { + public void testQueryAllWithNoSubtreeFlatteningAndNoSearchFilter() throws Exception { final Connection connection = newConnection(); final List resources = new LinkedList<>(); final QueryResponse result = @@ -106,79 +106,140 @@ public void testQueryAllWithNoSubtreeFlattening() throws Exception { 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"); + checkThatOrgUnitsExist(resources, "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"); + checkThatUsersExist(resources, 1, + "test1", + "test2", + "test3", + "test4", + "test5", + "test6" + ); + } - assertThat(resources.get(4).getContent().get("_ou").isNull()); - assertThat(resources.get(4).getId()).isEqualTo("test4"); + @Test + public void testQueryAllWithSearchFilterAndNoSubtreeFlattening() throws Exception { + final Connection connection = newConnection(); + final List resources = new LinkedList<>(); + final QueryResponse result = + connection.query( + newAuthConnectionContext(), + newQueryRequest("top-level-users").setQueryFilter(NO_FILTER), + resources); - assertThat(resources.get(5).getContent().get("_ou").isNull()); - assertThat(resources.get(5).getId()).isEqualTo("test5"); + assertThat(resources).hasSize(6); + assertThat(result.getPagedResultsCookie()).isNull(); + assertThat(result.getTotalPagedResults()).isEqualTo(-1); - assertThat(resources.get(6).getContent().get("_ou").isNull()); - assertThat(resources.get(6).getId()).isEqualTo("test6"); + checkThatUsersExist(resources, + "test1", + "test2", + "test3", + "test4", + "test5", + "test6" + ); } @Test - public void testQueryAllWithSubtreeFlattening() throws Exception { + public void testQueryAllWithSubtreeFlatteningAndNoSearchFilter() throws Exception { final Connection connection = newConnection(); final List resources = new LinkedList<>(); final QueryResponse result = connection.query( newAuthConnectionContext(), - newQueryRequest("all-users").setQueryFilter(NO_FILTER), + newQueryRequest("all-entries").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"); + checkThatOrgUnitsExist(resources, + "level1", + "level2" + ); - assertThat(resources.get(2).getContent().get("_ou").isNull()); - assertThat(resources.get(2).getId()).isEqualTo("sub2"); + checkThatUsersExist(resources, 2, + "sub2", + "sub1", + "test1", + "test2", + "test3", + "test4", + "test5", + "test6" + ); + } - assertThat(resources.get(3).getContent().get("_ou").isNull()); - assertThat(resources.get(3).getId()).isEqualTo("sub1"); + @Test + public void testQueryAllWithSubtreeFlatteningAndSearchFilter() throws Exception { + final Connection connection = newConnection(); + final List resources = new LinkedList<>(); + final QueryResponse result = + connection.query( + newAuthConnectionContext(), + newQueryRequest("all-users").setQueryFilter(NO_FILTER), + resources); - assertThat(resources.get(4).getContent().get("_ou").isNull()); - assertThat(resources.get(4).getId()).isEqualTo("test1"); + assertThat(resources).hasSize(8); + assertThat(result.getPagedResultsCookie()).isNull(); + assertThat(result.getTotalPagedResults()).isEqualTo(-1); - assertThat(resources.get(5).getContent().get("_ou").isNull()); - assertThat(resources.get(5).getId()).isEqualTo("test2"); + checkThatUsersExist(resources, + "sub2", + "sub1", + "test1", + "test2", + "test3", + "test4", + "test5", + "test6" + ); + } - assertThat(resources.get(6).getContent().get("_ou").isNull()); - assertThat(resources.get(6).getId()).isEqualTo("test3"); + @Test + public void testQueryNoneWithNoSubtreeFlatteningAndNoSearchFilter() throws Exception { + final Connection connection = newConnection(); + final List resources = new LinkedList<>(); + final QueryResponse result = + connection.query( + newAuthConnectionContext(), + newQueryRequest("").setQueryFilter(QueryFilter. alwaysFalse()), + resources); - assertThat(resources.get(7).getContent().get("_ou").isNull()); - assertThat(resources.get(7).getId()).isEqualTo("test4"); + assertThat(resources).hasSize(0); + assertThat(result.getPagedResultsCookie()).isNull(); + assertThat(result.getTotalPagedResults()).isEqualTo(-1); + } - assertThat(resources.get(8).getContent().get("_ou").isNull()); - assertThat(resources.get(8).getId()).isEqualTo("test5"); + @Test + public void testQueryNoneWithSearchFilterAndNoSubtreeFlattening() throws Exception { + final Connection connection = newConnection(); + final List resources = new LinkedList<>(); + final QueryResponse result = + connection.query( + newAuthConnectionContext(), + newQueryRequest("top-level-users") + .setQueryFilter(QueryFilter. alwaysFalse()), + resources); - assertThat(resources.get(9).getContent().get("_ou").isNull()); - assertThat(resources.get(9).getId()).isEqualTo("test6"); + assertThat(resources).hasSize(0); + assertThat(result.getPagedResultsCookie()).isNull(); + assertThat(result.getTotalPagedResults()).isEqualTo(-1); } @Test - public void testQueryNoneWithNoSubtreeFlattening() throws Exception { + public void testQueryNoneWithSubtreeFlatteningAndNoSearchFilter() throws Exception { final Connection connection = newConnection(); final List resources = new LinkedList<>(); - final QueryResponse result = connection.query(newAuthConnectionContext(), - newQueryRequest("").setQueryFilter(QueryFilter. alwaysFalse()), resources); + final QueryResponse result = + connection.query( + newAuthConnectionContext(), + newQueryRequest("all-entries") + .setQueryFilter(QueryFilter. alwaysFalse()), + resources); assertThat(resources).hasSize(0); assertThat(result.getPagedResultsCookie()).isNull(); @@ -186,7 +247,7 @@ public void testQueryNoneWithNoSubtreeFlattening() throws Exception { } @Test - public void testQueryNoneWithSubtreeFlattening() throws Exception { + public void testQueryNoneWithSubtreeFlatteningAndSearchFilter() throws Exception { final Connection connection = newConnection(); final List resources = new LinkedList<>(); final QueryResponse result = connection.query(newAuthConnectionContext(), @@ -198,7 +259,8 @@ public void testQueryNoneWithSubtreeFlattening() throws Exception { } @Test - public void testQueryPageResultsCookie() throws Exception { + public void testQueryPageResultsCookieWithNoSubtreeFlatteningAndNoSearchFilter() + throws Exception { final Connection connection = newConnection(); final List resources = new ArrayList<>(); @@ -214,14 +276,11 @@ public void testQueryPageResultsCookie() throws Exception { assertThat(result.getPagedResultsCookie()).isNotNull(); assertThat(resources).hasSize(3); - assertThat(resources.get(0).getContent().get("_ou").isNotNull()); - assertThat(resources.get(0).getContent().get("_ou").asString()).isEqualTo("level1"); + checkThatOrgUnitsExist(resources, "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"); + checkThatUsersExist(resources, 1, + "test1", + "test2"); String cookie = result.getPagedResultsCookie(); @@ -240,14 +299,10 @@ public void testQueryPageResultsCookie() throws Exception { assertThat(result.getPagedResultsCookie()).isNotNull(); assertThat(resources).hasSize(3); - assertThat(resources.get(0).getContent().get("_ou").isNull()); - assertThat(resources.get(0).getId()).isEqualTo("test3"); - - assertThat(resources.get(1).getContent().get("_ou").isNull()); - assertThat(resources.get(1).getId()).isEqualTo("test4"); - - assertThat(resources.get(2).getContent().get("_ou").isNull()); - assertThat(resources.get(2).getId()).isEqualTo("test5"); + checkThatUsersExist(resources, + "test3", + "test4", + "test5"); cookie = result.getPagedResultsCookie(); @@ -266,12 +321,62 @@ public void testQueryPageResultsCookie() throws Exception { assertThat(result.getPagedResultsCookie()).isNull(); assertThat(resources).hasSize(1); - assertThat(resources.get(0).getContent().get("_ou").isNull()); - assertThat(resources.get(0).getId()).isEqualTo("test6"); + checkThatUsersExist(resources, + "test6"); + } + + @Test + public void testQueryPageResultsCookieWithSubtreeFlatteningAndSearchFilter() throws Exception { + final Connection connection = newConnection(); + final List resources = new ArrayList<>(); + + // Read first page. + QueryResponse result = + connection.query( + newAuthConnectionContext(), + newQueryRequest("all-users") + .setQueryFilter(NO_FILTER) + .setPageSize(5), + resources); + + assertThat(result.getPagedResultsCookie()).isNotNull(); + assertThat(resources).hasSize(5); + + checkThatUsersExist(resources, + "sub2", + "sub1", + "test1", + "test2", + "test3"); + + String cookie = result.getPagedResultsCookie(); + + resources.clear(); + + // Read second page. + result = + connection.query( + newAuthConnectionContext(), + newQueryRequest("all-users") + .setQueryFilter(NO_FILTER) + .setPageSize(5) + .setPagedResultsCookie(cookie), + resources); + + assertThat(result.getPagedResultsCookie()).isNull(); + assertThat(resources).hasSize(3); + + checkThatUsersExist(resources, + "test4", + "test5", + "test6"); + + resources.clear(); } @Test - public void testQueryPageResultsIndexed() throws Exception { + public void testQueryPageResultsIndexedWithNoSubtreeFlatteningAndNoSearchFilter() + throws Exception { final Connection connection = newConnection(); final List resources = new ArrayList<>(); @@ -286,8 +391,33 @@ public void testQueryPageResultsIndexed() throws Exception { assertThat(result.getPagedResultsCookie()).isNotNull(); assertThat(resources).hasSize(2); - assertThat(resources.get(0).getId()).isEqualTo("test2"); - assertThat(resources.get(1).getId()).isEqualTo("test3"); + + checkThatUsersExist(resources, + "test2", + "test3"); + } + + @Test + public void testQueryPageResultsIndexedWithSubtreeFlatteningAndSearchFilter() throws Exception { + final Connection connection = newConnection(); + final List resources = new ArrayList<>(); + + QueryResponse result = + connection.query( + newAuthConnectionContext(), + newQueryRequest("all-users") + .setQueryFilter(NO_FILTER) + .setPageSize(3) + .setPagedResultsOffset(1), + resources); + + assertThat(result.getPagedResultsCookie()).isNotNull(); + assertThat(resources).hasSize(3); + + checkThatUsersExist(resources, + "test2", + "test3", + "test4"); } @Test(expectedExceptions = NotFoundException.class) @@ -867,13 +997,27 @@ private Rest2Ldap usersApi() throws IOException { collectionOf("user") .dnTemplate("dc=test") .useClientDnNaming("uid")) + .subResource( + collectionOf("user") + .urlTemplate("top-level-users") + .dnTemplate("dc=test") + .useClientDnNaming("uid") + .baseSearchFilter("(objectClass=person)")) + .subResource( + collectionOf("user") + .urlTemplate("all-entries") + .dnTemplate("dc=test") + .useClientDnNaming("uid") + .isReadOnly(true) + .flattenSubtree(true)) .subResource( collectionOf("user") .urlTemplate("all-users") .dnTemplate("dc=test") .useClientDnNaming("uid") .isReadOnly(true) - .flattenSubtree(true)), + .flattenSubtree(true) + .baseSearchFilter("(objectClass=person)")), resource("user") .objectClasses("top", "person") .property( @@ -915,6 +1059,38 @@ private void checkResourcesAreEqual(final ResourceResponse actual, final JsonVal .isEqualTo(expectedResource.getContent().getObject()); } + private void checkThatOrgUnitsExist(final List resources, + final String... orgUnitIds) { + checkThatOrgUnitsExist(resources, 0, orgUnitIds); + } + + private void checkThatOrgUnitsExist(final List resources, + final int startingIndex, + final String... expectedOrgUnitIds) { + for (int orgUnitIndex = 0; orgUnitIndex < expectedOrgUnitIds.length; ++orgUnitIndex) { + final ResourceResponse resource = resources.get(startingIndex + orgUnitIndex); + final JsonValue orgUnitId = resource.getContent().get("_ou"); + + assertThat(orgUnitId).isNotNull(); + assertThat(orgUnitId.asString()).isEqualTo(expectedOrgUnitIds[orgUnitIndex]); + } + } + + private void checkThatUsersExist(final List resources, + final String... expectedUserIds) { + checkThatUsersExist(resources, 0, expectedUserIds); + } + + private void checkThatUsersExist(final List resources, + final int startingIndex, final String... expectedUserIds) { + for (int userIndex = 0; userIndex < expectedUserIds.length; ++userIndex) { + final ResourceResponse resource = resources.get(startingIndex + userIndex); + + assertThat(resource.getContent().get("_ou").isNull()); + assertThat(resource.getId()).isEqualTo(expectedUserIds[userIndex]); + } + } + private AuthenticatedConnectionContext newAuthConnectionContext() throws IOException { return newAuthConnectionContext(new ArrayList()); } From 71a180997c65b0b5d23453e1985ecc6225bb4a42 Mon Sep 17 00:00:00 2001 From: Guy Paddock Date: Fri, 27 Oct 2017 00:26:04 -0400 Subject: [PATCH 12/15] Extends configurator for search filter support - Adds logic to configure base search filter using the new `baseSearchFilter` option for collection sub-resources. - Re-factors a bit of sub-resource configuration to make the code easier to follow and less of a mess. --- .../rest2ldap/endpoints/api/example-v1.json | 6 +- .../rest2ldap/Rest2LdapJsonConfigurator.java | 103 ++++++++++------- .../Rest2LdapJsonConfiguratorTest.java | 105 ++++++++++++++++-- .../rest2ldap/endpoints/api/example-v1.json | 6 +- 4 files changed, 170 insertions(+), 50 deletions(-) diff --git a/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/rest2ldap/endpoints/api/example-v1.json b/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/rest2ldap/endpoints/api/example-v1.json index 90de7e3c99..0bb2c83d1b 100644 --- a/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/rest2ldap/endpoints/api/example-v1.json +++ b/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/rest2ldap/endpoints/api/example-v1.json @@ -35,7 +35,8 @@ }, // This resource provides a read-only view of all users in the system, including // users nested underneath entries like org units, organizations, etc., starting - // from "ou=people,dc=example,dc=com" and working down. + // from "ou=people,dc=example,dc=com" and working down. It filters out any other + // structural elements, including organizations, org units, etc. "all-users": { "type": "collection", "dnTemplate": "ou=people,dc=example,dc=com", @@ -45,7 +46,8 @@ "dnAttribute": "uid" }, "isReadOnly": true, - "flattenSubtree": true + "flattenSubtree": true, + "baseSearchFilter": "(objectClass=person)" }, "groups": { "type": "collection", 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 5f83434369..93520e89c1 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 @@ -311,54 +311,79 @@ private static SubResource configureSubResource(final String urlTemplate, 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(); 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) - .dnTemplate(dnTemplate) - .isReadOnly(isReadOnly) - .glueObjectClasses(glueObjectClasses) - .flattenSubtree(flattenSubtree); - - final JsonValue namingStrategy = config.get("namingStrategy").required(); - final NamingStrategyType namingStrategyType = - namingStrategy.get("type").required().as(enumConstant(NamingStrategyType.class)); - - switch (namingStrategyType) { - case CLIENTDNNAMING: - collection.useClientDnNaming(namingStrategy.get("dnAttribute").required().asString()); - break; - case CLIENTNAMING: - collection.useClientNaming(namingStrategy.get("dnAttribute").required().asString(), - namingStrategy.get("idAttribute").required().asString()); - break; - case SERVERNAMING: - collection.useServerNaming(namingStrategy.get("dnAttribute").required().asString(), - namingStrategy.get("idAttribute").required().asString()); - break; - } - - return collection; + return configureCollectionSubResource( + config, resourceId, urlTemplate, dnTemplate, isReadOnly); } else { - return singletonOf(resourceId) - .urlTemplate(urlTemplate) - .dnTemplate(dnTemplate) - .isReadOnly(isReadOnly); + return configureSingletonSubResource( + config, resourceId, urlTemplate, dnTemplate, isReadOnly); } } + private static SubResource configureCollectionSubResource(final JsonValue config, + final String resourceId, + final String urlTemplate, + final String dnTemplate, + final Boolean isReadOnly) { + final String[] glueObjectClasses = + config.get("glueObjectClasses") + .defaultTo(emptyList()) + .asList(String.class) + .toArray(new String[0]); + + final Boolean flattenSubtree = config.get("flattenSubtree").defaultTo(false).asBoolean(); + final String searchFilter = config.get("baseSearchFilter").asString(); + + final SubResourceCollection collection = + collectionOf(resourceId) + .urlTemplate(urlTemplate) + .dnTemplate(dnTemplate) + .isReadOnly(isReadOnly) + .glueObjectClasses(glueObjectClasses) + .flattenSubtree(flattenSubtree) + .baseSearchFilter(searchFilter); + + configureCollectionNamingStrategy(config, collection); + + return collection; + } + + private static void configureCollectionNamingStrategy(final JsonValue config, + final SubResourceCollection collection) { + final JsonValue namingStrategy = config.get("namingStrategy").required(); + final NamingStrategyType namingStrategyType = + namingStrategy.get("type").required().as(enumConstant(NamingStrategyType.class)); + + switch (namingStrategyType) { + case CLIENTDNNAMING: + collection.useClientDnNaming(namingStrategy.get("dnAttribute").required().asString()); + break; + case CLIENTNAMING: + collection.useClientNaming(namingStrategy.get("dnAttribute").required().asString(), + namingStrategy.get("idAttribute").required().asString()); + break; + case SERVERNAMING: + collection.useServerNaming(namingStrategy.get("dnAttribute").required().asString(), + namingStrategy.get("idAttribute").required().asString()); + break; + } + } + + private static SubResource configureSingletonSubResource(final JsonValue config, + final String resourceId, + final String urlTemplate, + final String dnTemplate, + final Boolean isReadOnly) { + return singletonOf(resourceId) + .urlTemplate(urlTemplate) + .dnTemplate(dnTemplate) + .isReadOnly(isReadOnly); + } + private static PropertyMapper configurePropertyMapper(final JsonValue mapper, final String defaultLdapAttribute) { switch (mapper.get("type").required().asString()) { case "resourceType": diff --git a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfiguratorTest.java b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfiguratorTest.java index 2a74a2d4a7..e4bda7b20b 100644 --- a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfiguratorTest.java +++ b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfiguratorTest.java @@ -41,6 +41,7 @@ import org.forgerock.json.JsonValue; import org.forgerock.json.resource.Request; import org.forgerock.json.resource.RequestHandler; +import org.forgerock.opendj.ldap.Filter; import org.forgerock.services.context.Context; import org.forgerock.services.context.RootContext; import org.forgerock.testng.ForgeRockTestCase; @@ -136,7 +137,7 @@ public void testConfigureEndpointsWithApiDescription() throws Exception { } @DataProvider - public Object[][] invalidSubResourceConfigurations() { + public Object[][] invalidSubResourceSubtreeFlatteningConfigurations() { // @Checkstyle:off return new Object[][] { { @@ -187,6 +188,48 @@ public Object[][] validSubResourceConfigurations() { { false, false, + null, + "{" + + "'example-v1': {" + + "'subResources': {" + + "'all-users': {" + + "'type': 'collection'," + + "'dnTemplate': 'ou=people,dc=example,dc=com'," + + "'resource': 'frapi:opendj:rest2ldap:user:1.0'," + + "'namingStrategy': {" + + "'type': 'clientDnNaming'," + + "'dnAttribute': 'uid'" + + "}" + + "}" + + "}" + + "}" + + "}" + }, + { + false, + false, + "(objectClass=person)", + "{" + + "'example-v1': {" + + "'subResources': {" + + "'all-users': {" + + "'type': 'collection'," + + "'dnTemplate': 'ou=people,dc=example,dc=com'," + + "'resource': 'frapi:opendj:rest2ldap:user:1.0'," + + "'namingStrategy': {" + + "'type': 'clientDnNaming'," + + "'dnAttribute': 'uid'" + + "}," + + "'baseSearchFilter': '(objectClass=person)'" + + "}" + + "}" + + "}" + + "}" + }, + { + false, + false, + null, "{" + "'example-v1': {" + "'subResources': {" @@ -207,6 +250,7 @@ public Object[][] validSubResourceConfigurations() { { true, false, + null, "{" + "'example-v1': {" + "'subResources': {" @@ -227,6 +271,7 @@ public Object[][] validSubResourceConfigurations() { { true, false, + null, "{" + "'example-v1': {" + "'subResources': {" @@ -248,6 +293,7 @@ public Object[][] validSubResourceConfigurations() { { false, false, + null, "{" + "'example-v1': {" + "'subResources': {" @@ -269,6 +315,7 @@ public Object[][] validSubResourceConfigurations() { { true, true, + null, "{" + "'example-v1': {" + "'subResources': {" @@ -291,8 +338,9 @@ public Object[][] validSubResourceConfigurations() { // @Checkstyle:on } - @Test(dataProvider = "invalidSubResourceConfigurations") - public void testInvalidSubResourceConfigurations(final String rawJson) throws Exception { + @Test(dataProvider = "invalidSubResourceSubtreeFlatteningConfigurations") + public void testInvalidSubResourceSubtreeFlatteningConfigurations(final String rawJson) + throws Exception { try { Rest2LdapJsonConfigurator.configureResources(parseJson(rawJson)); @@ -304,9 +352,44 @@ public void testInvalidSubResourceConfigurations(final String rawJson) throws Ex } } + @Test + public void testInvalidSubResourceSearchFilterConfigurations() + throws Exception { + final String rawJson = + "{" + + "'example-v1': {" + + "'subResources': {" + + "'all-users': {" + + "'type': 'collection'," + + "'dnTemplate': 'ou=people,dc=example,dc=com'," + + "'resource': 'frapi:opendj:rest2ldap:user:1.0'," + + "'namingStrategy': {" + + "'type': 'clientDnNaming'," + + "'dnAttribute': 'uid'" + + "}," + + "'baseSearchFilter': 'badFilter'" + + "}" + + "}" + + "}" + + "}"; + + try { + Rest2LdapJsonConfigurator.configureResources(parseJson(rawJson)); + + fail("Expected an IllegalArgumentException"); + } + catch (IllegalArgumentException ex) { + assertThat(ex.getMessage()) + .isEqualTo( + "The provided search filter \"badFilter\" was missing an equal sign in the " + + "suspected simple filter component between positions 0 and 9"); + } + } + @Test(dataProvider = "validSubResourceConfigurations") - public void testValidSubResourceConfigurations(final boolean expectingReadOnly, - final boolean expectingSubtreeFlattened, + public void testValidSubResourceConfigurations(final boolean expectedReadOnly, + final boolean expectedSubtreeFlattened, + final String expectedSearchFilter, final String rawJson) throws Exception { final List resources = Rest2LdapJsonConfigurator.configureResources(parseJson(rawJson)); @@ -326,8 +409,16 @@ public void testValidSubResourceConfigurations(final boolean expectingReadOnly, allUsersSubResource = (SubResourceCollection)subResources.get("all-users"); - assertThat(allUsersSubResource.isReadOnly()).isEqualTo(expectingReadOnly); - assertThat(allUsersSubResource.shouldFlattenSubtree()).isEqualTo(expectingSubtreeFlattened); + assertThat(allUsersSubResource.isReadOnly()).isEqualTo(expectedReadOnly); + assertThat(allUsersSubResource.shouldFlattenSubtree()).isEqualTo(expectedSubtreeFlattened); + + if (expectedSearchFilter == null) { + assertThat(allUsersSubResource.getBaseSearchFilter()).isNull(); + } + else { + assertThat(allUsersSubResource.getBaseSearchFilter().toString()) + .isEqualTo(expectedSearchFilter); + } } private RequestHandler createRequestHandler(final File endpointsDir) throws IOException { diff --git a/opendj-server-legacy/resource/config/rest2ldap/endpoints/api/example-v1.json b/opendj-server-legacy/resource/config/rest2ldap/endpoints/api/example-v1.json index 90de7e3c99..0bb2c83d1b 100644 --- a/opendj-server-legacy/resource/config/rest2ldap/endpoints/api/example-v1.json +++ b/opendj-server-legacy/resource/config/rest2ldap/endpoints/api/example-v1.json @@ -35,7 +35,8 @@ }, // This resource provides a read-only view of all users in the system, including // users nested underneath entries like org units, organizations, etc., starting - // from "ou=people,dc=example,dc=com" and working down. + // from "ou=people,dc=example,dc=com" and working down. It filters out any other + // structural elements, including organizations, org units, etc. "all-users": { "type": "collection", "dnTemplate": "ou=people,dc=example,dc=com", @@ -45,7 +46,8 @@ "dnAttribute": "uid" }, "isReadOnly": true, - "flattenSubtree": true + "flattenSubtree": true, + "baseSearchFilter": "(objectClass=person)" }, "groups": { "type": "collection", From 1a643d8375f43c12233711cc571084a0dc8b3006 Mon Sep 17 00:00:00 2001 From: Guy Paddock Date: Fri, 27 Oct 2017 00:33:25 -0400 Subject: [PATCH 13/15] Adds Rosie copyright to modded files --- .../org/forgerock/opendj/rest2ldap/ReadOnlyRequestHandler.java | 2 +- .../org/forgerock/opendj/rest2ldap/ReferencePropertyMapper.java | 1 + .../src/main/java/org/forgerock/opendj/rest2ldap/Resource.java | 1 + .../forgerock/opendj/rest2ldap/Rest2LdapJsonConfigurator.java | 2 +- .../main/java/org/forgerock/opendj/rest2ldap/SubResource.java | 2 +- .../org/forgerock/opendj/rest2ldap/SubResourceCollection.java | 2 +- .../java/org/forgerock/opendj/rest2ldap/SubResourceImpl.java | 1 + .../org/forgerock/opendj/rest2ldap/SubResourceSingleton.java | 1 + .../opendj/rest2ldap/OAuth2JsonConfigurationTestCase.java | 1 + 9 files changed, 9 insertions(+), 4 deletions(-) diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReadOnlyRequestHandler.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReadOnlyRequestHandler.java index f962a10eac..06fe696989 100644 --- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReadOnlyRequestHandler.java +++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReadOnlyRequestHandler.java @@ -12,7 +12,7 @@ * information: "Portions copyright [year] [name of copyright owner]". * * Copyright 2016 ForgeRock AS. - * + * Portions Copyright 2017 Rosie Applications, Inc. */ package org.forgerock.opendj.rest2ldap; diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferencePropertyMapper.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferencePropertyMapper.java index 2ceacad654..5d9712f377 100644 --- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferencePropertyMapper.java +++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferencePropertyMapper.java @@ -12,6 +12,7 @@ * information: "Portions Copyright [year] [name of copyright owner]". * * Copyright 2012-2016 ForgeRock AS. + * Portions Copyright 2017 Rosie Applications, Inc. */ package org.forgerock.opendj.rest2ldap; diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Resource.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Resource.java index e7964bd7d6..45676478cf 100644 --- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Resource.java +++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Resource.java @@ -12,6 +12,7 @@ * information: "Portions copyright [year] [name of copyright owner]". * * Copyright 2016 ForgeRock AS. + * Portions Copyright 2017 Rosie Applications, Inc. */ package org.forgerock.opendj.rest2ldap; 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 93520e89c1..1a794c7ad1 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 @@ -12,7 +12,7 @@ * information: "Portions copyright [year] [name of copyright owner]". * * Copyright 2016 ForgeRock AS. - * + * Portions Copyright 2017 Rosie Applications, Inc. */ package org.forgerock.opendj.rest2ldap; diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResource.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResource.java index ee6a16af36..4c964703e9 100644 --- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResource.java +++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResource.java @@ -12,7 +12,7 @@ * information: "Portions copyright [year] [name of copyright owner]". * * Copyright 2016 ForgeRock AS. - * + * Portions Copyright 2017 Rosie Applications, Inc. */ package org.forgerock.opendj.rest2ldap; 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 05b27200a3..0e27febd1d 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 @@ -12,10 +12,10 @@ * information: "Portions copyright [year] [name of copyright owner]". * * Copyright 2016 ForgeRock AS. + * Portions Copyright 2017 Rosie Applications, Inc. */ package org.forgerock.opendj.rest2ldap; -import static org.forgerock.guava.common.base.Preconditions.checkNotNull; import static org.forgerock.http.routing.RoutingMode.EQUALS; import static org.forgerock.http.routing.RoutingMode.STARTS_WITH; import static org.forgerock.json.resource.RouteMatchers.requestUriMatcher; 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 4b65ea9a35..1f83f00039 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 @@ -12,6 +12,7 @@ * information: "Portions Copyright [year] [name of copyright owner]". * * Copyright 2012-2016 ForgeRock AS. + * Portions Copyright 2017 Rosie Applications, Inc. */ package org.forgerock.opendj.rest2ldap; 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 92a1f338d9..6182a244ad 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 @@ -12,6 +12,7 @@ * information: "Portions copyright [year] [name of copyright owner]". * * Copyright 2016 ForgeRock AS. + * Portions Copyright 2017 Rosie Applications, Inc. */ package org.forgerock.opendj.rest2ldap; diff --git a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/OAuth2JsonConfigurationTestCase.java b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/OAuth2JsonConfigurationTestCase.java index 7c10cae42d..0f0785a3ca 100644 --- a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/OAuth2JsonConfigurationTestCase.java +++ b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/OAuth2JsonConfigurationTestCase.java @@ -12,6 +12,7 @@ * information: "Portions Copyright [year] [name of copyright owner]". * * Copyright 2016 ForgeRock AS. + * Portions Copyright 2017 Rosie Applications, Inc. */ package org.forgerock.opendj.rest2ldap; From dfd12273bf827c53385f28e27223fae8fd9a8868 Mon Sep 17 00:00:00 2001 From: Guy Paddock Date: Fri, 27 Oct 2017 01:23:02 -0400 Subject: [PATCH 14/15] Declare search filter `final` --- .../java/org/forgerock/opendj/rest2ldap/SubResourceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 1f83f00039..f731a1b732 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 @@ -141,7 +141,7 @@ final class SubResourceImpl { private final Resource resource; private final Attribute glueObjectClasses; private final boolean flattenSubtree; - private Filter baseSearchFilter; + private final Filter baseSearchFilter; SubResourceImpl(final Rest2Ldap rest2Ldap, final DN baseDn, final Attribute glueObjectClasses, final NamingStrategy namingStrategy, final Resource resource) { From b8f565be8a4a965ff6328ed8f79f17e98450f4b1 Mon Sep 17 00:00:00 2001 From: Guy Paddock Date: Sun, 29 Oct 2017 20:07:02 -0400 Subject: [PATCH 15/15] Fixes indent on data providers --- .../Rest2LdapJsonConfiguratorTest.java | 381 +++++++++--------- 1 file changed, 191 insertions(+), 190 deletions(-) diff --git a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfiguratorTest.java b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfiguratorTest.java index e4bda7b20b..f948f306df 100644 --- a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfiguratorTest.java +++ b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfiguratorTest.java @@ -12,6 +12,7 @@ * information: "Portions Copyright [year] [name of copyright owner]". * * Copyright 2016 ForgeRock AS. + * Portions Copyright 2017 Rosie Applications, Inc. */ package org.forgerock.opendj.rest2ldap; @@ -140,43 +141,43 @@ public void testConfigureEndpointsWithApiDescription() throws Exception { public Object[][] invalidSubResourceSubtreeFlatteningConfigurations() { // @Checkstyle:off return new Object[][] { - { - "{" - + "'example-v1': {" - + "'subResources': {" - + "'writeable-collection': {" - + "'type': 'collection'," - + "'dnTemplate': 'ou=people,dc=example,dc=com'," - + "'resource': 'frapi:opendj:rest2ldap:user:1.0'," - + "'namingStrategy': {" - + "'type': 'clientDnNaming'," - + "'dnAttribute': 'uid'" - + "}," - + "'flattenSubtree': true" - + "}" - + "}" - + "}" + { + "{" + + "'example-v1': {" + + "'subResources': {" + + "'writeable-collection': {" + + "'type': 'collection'," + + "'dnTemplate': 'ou=people,dc=example,dc=com'," + + "'resource': 'frapi:opendj:rest2ldap:user:1.0'," + + "'namingStrategy': {" + + "'type': 'clientDnNaming'," + + "'dnAttribute': 'uid'" + + "}," + + "'flattenSubtree': true" + + "}" + "}" - }, - { - "{" - + "'example-v1': {" - + "'subResources': {" - + "'writeable-collection': {" - + "'type': 'collection'," - + "'dnTemplate': 'ou=people,dc=example,dc=com'," - + "'resource': 'frapi:opendj:rest2ldap:user:1.0'," - + "'namingStrategy': {" - + "'type': 'clientDnNaming'," - + "'dnAttribute': 'uid'" - + "}," - + "'isReadOnly': false," - + "'flattenSubtree': true" - + "}" - + "}" - + "}" + + "}" + + "}" + }, + { + "{" + + "'example-v1': {" + + "'subResources': {" + + "'writeable-collection': {" + + "'type': 'collection'," + + "'dnTemplate': 'ou=people,dc=example,dc=com'," + + "'resource': 'frapi:opendj:rest2ldap:user:1.0'," + + "'namingStrategy': {" + + "'type': 'clientDnNaming'," + + "'dnAttribute': 'uid'" + + "}," + + "'isReadOnly': false," + + "'flattenSubtree': true" + + "}" + "}" - } + + "}" + + "}" + } }; // @Checkstyle:on } @@ -185,155 +186,155 @@ public Object[][] invalidSubResourceSubtreeFlatteningConfigurations() { public Object[][] validSubResourceConfigurations() { // @Checkstyle:off return new Object[][] { - { - false, - false, - null, - "{" - + "'example-v1': {" - + "'subResources': {" - + "'all-users': {" - + "'type': 'collection'," - + "'dnTemplate': 'ou=people,dc=example,dc=com'," - + "'resource': 'frapi:opendj:rest2ldap:user:1.0'," - + "'namingStrategy': {" - + "'type': 'clientDnNaming'," - + "'dnAttribute': 'uid'" - + "}" - + "}" - + "}" + { + false, + false, + null, + "{" + + "'example-v1': {" + + "'subResources': {" + + "'all-users': {" + + "'type': 'collection'," + + "'dnTemplate': 'ou=people,dc=example,dc=com'," + + "'resource': 'frapi:opendj:rest2ldap:user:1.0'," + + "'namingStrategy': {" + + "'type': 'clientDnNaming'," + + "'dnAttribute': 'uid'" + "}" + + "}" + "}" - }, - { - false, - false, - "(objectClass=person)", - "{" - + "'example-v1': {" - + "'subResources': {" - + "'all-users': {" - + "'type': 'collection'," - + "'dnTemplate': 'ou=people,dc=example,dc=com'," - + "'resource': 'frapi:opendj:rest2ldap:user:1.0'," - + "'namingStrategy': {" - + "'type': 'clientDnNaming'," - + "'dnAttribute': 'uid'" - + "}," - + "'baseSearchFilter': '(objectClass=person)'" - + "}" - + "}" - + "}" + + "}" + + "}" + }, + { + false, + false, + "(objectClass=person)", + "{" + + "'example-v1': {" + + "'subResources': {" + + "'all-users': {" + + "'type': 'collection'," + + "'dnTemplate': 'ou=people,dc=example,dc=com'," + + "'resource': 'frapi:opendj:rest2ldap:user:1.0'," + + "'namingStrategy': {" + + "'type': 'clientDnNaming'," + + "'dnAttribute': 'uid'" + + "}," + + "'baseSearchFilter': '(objectClass=person)'" + + "}" + "}" - }, - { - false, - false, - null, - "{" - + "'example-v1': {" - + "'subResources': {" - + "'all-users': {" - + "'type': 'collection'," - + "'dnTemplate': 'ou=people,dc=example,dc=com'," - + "'resource': 'frapi:opendj:rest2ldap:user:1.0'," - + "'namingStrategy': {" - + "'type': 'clientDnNaming'," - + "'dnAttribute': 'uid'" - + "}," - + "'flattenSubtree': false" - + "}" - + "}" - + "}" + + "}" + + "}" + }, + { + false, + false, + null, + "{" + + "'example-v1': {" + + "'subResources': {" + + "'all-users': {" + + "'type': 'collection'," + + "'dnTemplate': 'ou=people,dc=example,dc=com'," + + "'resource': 'frapi:opendj:rest2ldap:user:1.0'," + + "'namingStrategy': {" + + "'type': 'clientDnNaming'," + + "'dnAttribute': 'uid'" + + "}," + + "'flattenSubtree': false" + + "}" + "}" - }, - { - true, - false, - null, - "{" - + "'example-v1': {" - + "'subResources': {" - + "'all-users': {" - + "'type': 'collection'," - + "'dnTemplate': 'ou=people,dc=example,dc=com'," - + "'resource': 'frapi:opendj:rest2ldap:user:1.0'," - + "'namingStrategy': {" - + "'type': 'clientDnNaming'," - + "'dnAttribute': 'uid'" - + "}," - + "'isReadOnly': true" - + "}" - + "}" - + "}" + + "}" + + "}" + }, + { + true, + false, + null, + "{" + + "'example-v1': {" + + "'subResources': {" + + "'all-users': {" + + "'type': 'collection'," + + "'dnTemplate': 'ou=people,dc=example,dc=com'," + + "'resource': 'frapi:opendj:rest2ldap:user:1.0'," + + "'namingStrategy': {" + + "'type': 'clientDnNaming'," + + "'dnAttribute': 'uid'" + + "}," + + "'isReadOnly': true" + + "}" + "}" - }, - { - true, - false, - null, - "{" - + "'example-v1': {" - + "'subResources': {" - + "'all-users': {" - + "'type': 'collection'," - + "'dnTemplate': 'ou=people,dc=example,dc=com'," - + "'resource': 'frapi:opendj:rest2ldap:user:1.0'," - + "'namingStrategy': {" - + "'type': 'clientDnNaming'," - + "'dnAttribute': 'uid'" - + "}," - + "'isReadOnly': true," - + "'flattenSubtree': false" - + "}" - + "}" - + "}" + + "}" + + "}" + }, + { + true, + false, + null, + "{" + + "'example-v1': {" + + "'subResources': {" + + "'all-users': {" + + "'type': 'collection'," + + "'dnTemplate': 'ou=people,dc=example,dc=com'," + + "'resource': 'frapi:opendj:rest2ldap:user:1.0'," + + "'namingStrategy': {" + + "'type': 'clientDnNaming'," + + "'dnAttribute': 'uid'" + + "}," + + "'isReadOnly': true," + + "'flattenSubtree': false" + + "}" + "}" - }, - { - false, - false, - null, - "{" - + "'example-v1': {" - + "'subResources': {" - + "'all-users': {" - + "'type': 'collection'," - + "'dnTemplate': 'ou=people,dc=example,dc=com'," - + "'resource': 'frapi:opendj:rest2ldap:user:1.0'," - + "'namingStrategy': {" - + "'type': 'clientDnNaming'," - + "'dnAttribute': 'uid'" - + "}," - + "'isReadOnly': false," - + "'flattenSubtree': false" - + "}" - + "}" - + "}" + + "}" + + "}" + }, + { + false, + false, + null, + "{" + + "'example-v1': {" + + "'subResources': {" + + "'all-users': {" + + "'type': 'collection'," + + "'dnTemplate': 'ou=people,dc=example,dc=com'," + + "'resource': 'frapi:opendj:rest2ldap:user:1.0'," + + "'namingStrategy': {" + + "'type': 'clientDnNaming'," + + "'dnAttribute': 'uid'" + + "}," + + "'isReadOnly': false," + + "'flattenSubtree': false" + + "}" + "}" - }, - { - true, - true, - null, - "{" - + "'example-v1': {" - + "'subResources': {" - + "'all-users': {" - + "'type': 'collection'," - + "'dnTemplate': 'ou=people,dc=example,dc=com'," - + "'resource': 'frapi:opendj:rest2ldap:user:1.0'," - + "'namingStrategy': {" - + "'type': 'clientDnNaming'," - + "'dnAttribute': 'uid'" - + "}," - + "'isReadOnly': true," - + "'flattenSubtree': true" - + "}" - + "}" - + "}" + + "}" + + "}" + }, + { + true, + true, + null, + "{" + + "'example-v1': {" + + "'subResources': {" + + "'all-users': {" + + "'type': 'collection'," + + "'dnTemplate': 'ou=people,dc=example,dc=com'," + + "'resource': 'frapi:opendj:rest2ldap:user:1.0'," + + "'namingStrategy': {" + + "'type': 'clientDnNaming'," + + "'dnAttribute': 'uid'" + + "}," + + "'isReadOnly': true," + + "'flattenSubtree': true" + + "}" + "}" - } + + "}" + + "}" + } }; // @Checkstyle:on } @@ -353,24 +354,24 @@ public void testInvalidSubResourceSubtreeFlatteningConfigurations(final String r } @Test - public void testInvalidSubResourceSearchFilterConfigurations() + public void testInvalidSubResourceSearchFilterConfiguration() throws Exception { final String rawJson = "{" - + "'example-v1': {" - + "'subResources': {" - + "'all-users': {" - + "'type': 'collection'," - + "'dnTemplate': 'ou=people,dc=example,dc=com'," - + "'resource': 'frapi:opendj:rest2ldap:user:1.0'," - + "'namingStrategy': {" - + "'type': 'clientDnNaming'," - + "'dnAttribute': 'uid'" - + "}," - + "'baseSearchFilter': 'badFilter'" - + "}" - + "}" + + "'example-v1': {" + + "'subResources': {" + + "'all-users': {" + + "'type': 'collection'," + + "'dnTemplate': 'ou=people,dc=example,dc=com'," + + "'resource': 'frapi:opendj:rest2ldap:user:1.0'," + + "'namingStrategy': {" + + "'type': 'clientDnNaming'," + + "'dnAttribute': 'uid'" + + "}," + + "'baseSearchFilter': 'badFilter'" + + "}" + "}" + + "}" + "}"; try {