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", 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 @@ protected <V> Promise<V, ResourceException> handleRequest(final Context context, final Request request) { return new BadRequestException(ERR_READ_ONLY_ENDPOINT.get().toString()).asPromise(); } @Override public ApiDescription api(ApiProducer<ApiDescription> producer) { if (delegate instanceof Describable) { return ((Describable<ApiDescription, Request>)delegate).api(producer); } else { return super.api(producer); } } } opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Resource.java
@@ -470,6 +470,31 @@ return id; } /** * 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 * {@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 @@ 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 @@ 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 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)); if (!isReadOnly) { resource.create(createOperation(CreateMode.ID_FROM_SERVER)); } resource.query(Query.query() .stability(EVOLVING) .type(QueryType.FILTER) @@ -580,23 +609,29 @@ 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(); } 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 @@ 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(); } private DescribableRequestHandler configureEndpoints(final File endpointsDir) throws Exception { final RequestHandler rh = Rest2LdapJsonConfigurator.configureEndpoints(endpointsDir, Options.defaultOptions()); DescribableRequestHandler handler = new DescribableRequestHandler(rh); 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 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.<String, String> 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); } } 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",