From e4c0edea06c8fee28369f03f393b7d54b2b6235c Mon Sep 17 00:00:00 2001
From: Jean-Noël Rouvignac <jean-noel.rouvignac@forgerock.com>
Date: Fri, 16 Sep 2016 13:25:06 +0000
Subject: [PATCH] OPENDJ-3246 Return the CREST descriptor over REST for rest2ldap endpoints
---
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Resource.java | 409 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 408 insertions(+), 1 deletions(-)
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 ab76ccd..07421a0 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,11 +12,18 @@
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2016 ForgeRock AS.
- *
*/
package org.forgerock.opendj.rest2ldap;
import static java.util.Arrays.asList;
+import static org.forgerock.api.enums.CountPolicy.*;
+import static org.forgerock.api.enums.PagingMode.*;
+import static org.forgerock.api.enums.ParameterSource.*;
+import static org.forgerock.api.enums.PatchOperation.*;
+import static org.forgerock.api.enums.Stability.*;
+import static org.forgerock.api.models.VersionedPath.*;
+import static org.forgerock.json.JsonValue.*;
+import static org.forgerock.json.resource.ResourceException.*;
import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_ABSTRACT_TYPE_IN_CREATE;
import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_MISSING_TYPE_PROPERTY_IN_CREATE;
import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_UNRECOGNIZED_RESOURCE_SUPER_TYPE;
@@ -34,6 +41,25 @@
import java.util.Map;
import java.util.Set;
+import org.forgerock.api.enums.CreateMode;
+import org.forgerock.api.enums.QueryType;
+import org.forgerock.api.models.ApiDescription;
+import org.forgerock.api.models.ApiError;
+import org.forgerock.api.models.Create;
+import org.forgerock.api.models.Definitions;
+import org.forgerock.api.models.Delete;
+import org.forgerock.api.models.Errors;
+import org.forgerock.api.models.Items;
+import org.forgerock.api.models.Parameter;
+import org.forgerock.api.models.Patch;
+import org.forgerock.api.models.Paths;
+import org.forgerock.api.models.Query;
+import org.forgerock.api.models.Read;
+import org.forgerock.api.models.Reference;
+import org.forgerock.api.models.Schema;
+import org.forgerock.api.models.Services;
+import org.forgerock.api.models.Update;
+import org.forgerock.http.ApiProducer;
import org.forgerock.i18n.LocalizedIllegalArgumentException;
import org.forgerock.json.JsonPointer;
import org.forgerock.json.JsonValue;
@@ -43,11 +69,33 @@
import org.forgerock.opendj.ldap.Attribute;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.LinkedAttribute;
+import org.forgerock.util.i18n.LocalizableString;
/**
* Defines the characteristics of a resource, including its properties, inheritance, and sub-resources.
*/
public final class Resource {
+ // Commons errors
+ private static final String ERROR_BAD_REQUEST = "frapi:common#/errors/badRequest";
+ private static final String ERROR_FORBIDDEN = "frapi:common#/errors/forbidden";
+ private static final String ERROR_INTERNAL_SERVER_ERROR = "frapi:common#/errors/internalServerError";
+ private static final String ERROR_NOT_FOUND = "frapi:common#/errors/notFound";
+ private static final String ERROR_REQUEST_ENTITY_TOO_LARGE = "frapi:common#/errors/requestEntityTooLarge";
+ private static final String ERROR_REQUEST_TIMEOUT = "frapi:common#/errors/requestTimeout";
+ private static final String ERROR_UNAUTHORIZED = "frapi:common#/errors/unauthorized";
+ private static final String ERROR_UNAVAILABLE = "frapi:common#/errors/unavailable";
+ private static final String ERROR_VERSION_MISMATCH = "frapi:common#/errors/versionMismatch";
+
+ // rest2ldap errors
+ private static final String ERROR_ADMIN_LIMIT_EXCEEDED = "#/errors/adminLimitExceeded";
+ private static final String ERROR_READ_FOUND_MULTIPLE_ENTRIES = "#/errors/readFoundMultipleEntries";
+ private static final String ERROR_PASSWORD_MODIFY_REQUIRES_HTTPS = "#/errors/passwordModifyRequiresHttps";
+ private static final String ERROR_PASSWORD_MODIFY_REQUIRES_AUTHENTICATION = "#/errors/passwordModifyRequiresAuthn";
+
+ /** All fields are queryable, but the directory server may reject some requests (unindexed?). */
+ private static final String ALL_FIELDS = "*";
+
+
/** The resource ID. */
private final String id;
/** {@code true} if only sub-types of this resource can be created. */
@@ -444,4 +492,363 @@
PropertyMapper getPropertyMapper() {
return propertyMapper;
}
+
+ /**
+ * Returns the api description that describes a single instance resource.
+ *
+ * @param isReadOnly
+ * whether the associated resource is read only
+ * @return a new api description that describes a single instance resource.
+ */
+ ApiDescription instanceApi(boolean isReadOnly) {
+ if (allProperties.isEmpty()) {
+ return null;
+ }
+
+ org.forgerock.api.models.Resource.Builder resource = org.forgerock.api.models.Resource.
+ resource()
+ .resourceSchema(schemaRef("#/definitions/" + id))
+ .mvccSupported(isMvccSupported());
+
+ resource.read(readOperation());
+ if (!isReadOnly) {
+ resource.update(updateOperation());
+ resource.patch(patchOperation());
+ for (Action action : supportedActions) {
+ resource.action(actions(action));
+ }
+ }
+
+ return ApiDescription.apiDescription()
+ .id("unused").version("unused")
+ .definitions(definitions())
+ .errors(errors())
+ .build();
+ }
+
+ /**
+ * Returns the api description that describes a collection resource.
+ *
+ * @param isReadOnly
+ * whether the associated resource is read only
+ * @return a new api description that describes a collection resource.
+ */
+ ApiDescription collectionApi(boolean isReadOnly) {
+ org.forgerock.api.models.Resource.Builder resource = org.forgerock.api.models.Resource.
+ resource()
+ .resourceSchema(schemaRef("#/definitions/" + id))
+ .mvccSupported(isMvccSupported());
+
+ resource.items(buildItems(isReadOnly));
+ resource.create(createOperation(CreateMode.ID_FROM_SERVER));
+ resource.query(Query.query()
+ .stability(EVOLVING)
+ .type(QueryType.FILTER)
+ .queryableFields(ALL_FIELDS)
+ .pagingModes(COOKIE, OFFSET)
+ .countPolicies(NONE)
+ .error(errorRef(ERROR_BAD_REQUEST))
+ .error(errorRef(ERROR_UNAUTHORIZED))
+ .error(errorRef(ERROR_FORBIDDEN))
+ .error(errorRef(ERROR_REQUEST_TIMEOUT))
+ .error(errorRef(ERROR_ADMIN_LIMIT_EXCEEDED))
+ .error(errorRef(ERROR_INTERNAL_SERVER_ERROR))
+ .error(errorRef(ERROR_UNAVAILABLE))
+ .build());
+
+ return ApiDescription.apiDescription()
+ .id("unused").version("unused")
+ .definitions(definitions())
+ .services(Services.services()
+ .put(id, resource.build())
+ .build())
+ .paths(getPaths())
+ .errors(errors())
+ .build();
+ }
+
+ private Paths getPaths() {
+ 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())
+ .build();
+ }
+
+ private Definitions definitions() {
+ final Definitions.Builder definitions = Definitions.definitions();
+ definitions.put(id, buildJsonSchema());
+ for (Resource subType : subTypes) {
+ definitions.put(subType.id, subType.buildJsonSchema());
+ }
+ return definitions.build();
+ }
+
+ /**
+ * Returns the api description that describes a resource with sub resources.
+ *
+ * @param producer
+ * the api producer
+ * @return a new api description that describes a resource with sub resources.
+ */
+ ApiDescription subResourcesApi(ApiProducer<ApiDescription> producer) {
+ return subResourceRouter.api(producer);
+ }
+
+ private boolean isMvccSupported() {
+ return allProperties.containsKey("_rev");
+ }
+
+ private Items buildItems(boolean isReadOnly) {
+ final Items.Builder builder = Items.items();
+ builder.pathParameter(Parameter
+ .parameter()
+ .name("id")
+ .type("string")
+ .source(PATH)
+ .required(true)
+ .build())
+ .read(readOperation());
+ if (!isReadOnly) {
+ builder.create(createOperation(CreateMode.ID_FROM_CLIENT));
+ builder.update(updateOperation());
+ builder.delete(deleteOperation());
+ builder.patch(patchOperation());
+ for (Action action : supportedActions) {
+ builder.action(actions(action));
+ }
+ }
+ return builder.build();
+ }
+
+ private org.forgerock.api.models.Action actions(Action action) {
+ switch (action) {
+ case MODIFY_PASSWORD:
+ return modifyPasswordAction();
+ case RESET_PASSWORD:
+ return resetPasswordAction();
+ default:
+ throw new RuntimeException("Not implemented for action " + action);
+ }
+ }
+
+ private static Create createOperation(CreateMode createMode) {
+ return Create.create()
+ .stability(EVOLVING)
+ .mode(createMode)
+ .error(errorRef(ERROR_BAD_REQUEST))
+ .error(errorRef(ERROR_UNAUTHORIZED))
+ .error(errorRef(ERROR_FORBIDDEN))
+ .error(errorRef(ERROR_NOT_FOUND))
+ .error(errorRef(ERROR_REQUEST_TIMEOUT))
+ .error(errorRef(ERROR_VERSION_MISMATCH))
+ .error(errorRef(ERROR_REQUEST_ENTITY_TOO_LARGE))
+ .error(errorRef(ERROR_ADMIN_LIMIT_EXCEEDED))
+ .error(errorRef(ERROR_INTERNAL_SERVER_ERROR))
+ .error(errorRef(ERROR_UNAVAILABLE))
+ .build();
+ }
+
+ private static Delete deleteOperation() {
+ return Delete.delete()
+ .stability(EVOLVING)
+ .error(errorRef(ERROR_BAD_REQUEST))
+ .error(errorRef(ERROR_UNAUTHORIZED))
+ .error(errorRef(ERROR_FORBIDDEN))
+ .error(errorRef(ERROR_NOT_FOUND))
+ .error(errorRef(ERROR_REQUEST_TIMEOUT))
+ .error(errorRef(ERROR_VERSION_MISMATCH))
+ .error(errorRef(ERROR_REQUEST_ENTITY_TOO_LARGE))
+ .error(errorRef(ERROR_READ_FOUND_MULTIPLE_ENTRIES))
+ .error(errorRef(ERROR_ADMIN_LIMIT_EXCEEDED))
+ .error(errorRef(ERROR_INTERNAL_SERVER_ERROR))
+ .error(errorRef(ERROR_UNAVAILABLE))
+ .build();
+ }
+
+ private static Patch patchOperation() {
+ return Patch.patch()
+ .stability(EVOLVING)
+ .operations(ADD, REMOVE, REPLACE, INCREMENT)
+ .error(errorRef(ERROR_BAD_REQUEST))
+ .error(errorRef(ERROR_UNAUTHORIZED))
+ .error(errorRef(ERROR_FORBIDDEN))
+ .error(errorRef(ERROR_NOT_FOUND))
+ .error(errorRef(ERROR_REQUEST_TIMEOUT))
+ .error(errorRef(ERROR_VERSION_MISMATCH))
+ .error(errorRef(ERROR_REQUEST_ENTITY_TOO_LARGE))
+ .error(errorRef(ERROR_READ_FOUND_MULTIPLE_ENTRIES))
+ .error(errorRef(ERROR_ADMIN_LIMIT_EXCEEDED))
+ .error(errorRef(ERROR_INTERNAL_SERVER_ERROR))
+ .error(errorRef(ERROR_UNAVAILABLE))
+ .build();
+ }
+
+ private static Read readOperation() {
+ return Read.read()
+ .stability(EVOLVING)
+ .error(errorRef(ERROR_BAD_REQUEST))
+ .error(errorRef(ERROR_UNAUTHORIZED))
+ .error(errorRef(ERROR_FORBIDDEN))
+ .error(errorRef(ERROR_NOT_FOUND))
+ .error(errorRef(ERROR_REQUEST_TIMEOUT))
+ .error(errorRef(ERROR_READ_FOUND_MULTIPLE_ENTRIES))
+ .error(errorRef(ERROR_ADMIN_LIMIT_EXCEEDED))
+ .error(errorRef(ERROR_INTERNAL_SERVER_ERROR))
+ .error(errorRef(ERROR_UNAVAILABLE))
+ .build();
+ }
+
+ private static Update updateOperation() {
+ return Update.update()
+ .stability(EVOLVING)
+ .error(errorRef(ERROR_BAD_REQUEST))
+ .error(errorRef(ERROR_UNAUTHORIZED))
+ .error(errorRef(ERROR_FORBIDDEN))
+ .error(errorRef(ERROR_NOT_FOUND))
+ .error(errorRef(ERROR_REQUEST_TIMEOUT))
+ .error(errorRef(ERROR_VERSION_MISMATCH))
+ .error(errorRef(ERROR_REQUEST_ENTITY_TOO_LARGE))
+ .error(errorRef(ERROR_READ_FOUND_MULTIPLE_ENTRIES))
+ .error(errorRef(ERROR_ADMIN_LIMIT_EXCEEDED))
+ .error(errorRef(ERROR_INTERNAL_SERVER_ERROR))
+ .error(errorRef(ERROR_UNAVAILABLE))
+ .build();
+ }
+
+ private static org.forgerock.api.models.Action modifyPasswordAction() {
+ return org.forgerock.api.models.Action.action()
+ .stability(EVOLVING)
+ .name("modifyPassword")
+ .request(passwordModifyRequest())
+ .description("Modify a user password. This action requires HTTPS.")
+ .error(errorRef(ERROR_BAD_REQUEST))
+ .error(errorRef(ERROR_UNAUTHORIZED))
+ .error(errorRef(ERROR_PASSWORD_MODIFY_REQUIRES_HTTPS))
+ .error(errorRef(ERROR_PASSWORD_MODIFY_REQUIRES_AUTHENTICATION))
+ .error(errorRef(ERROR_FORBIDDEN))
+ .error(errorRef(ERROR_NOT_FOUND))
+ .error(errorRef(ERROR_REQUEST_TIMEOUT))
+ .error(errorRef(ERROR_VERSION_MISMATCH))
+ .error(errorRef(ERROR_REQUEST_ENTITY_TOO_LARGE))
+ .error(errorRef(ERROR_READ_FOUND_MULTIPLE_ENTRIES))
+ .error(errorRef(ERROR_ADMIN_LIMIT_EXCEEDED))
+ .error(errorRef(ERROR_INTERNAL_SERVER_ERROR))
+ .error(errorRef(ERROR_UNAVAILABLE))
+ .build();
+ }
+
+ private static org.forgerock.api.models.Schema passwordModifyRequest() {
+ final JsonValue jsonSchema = json(object(
+ field("type", "object"),
+ field("description", "Supply the old password and new password."),
+ field("required", array("oldPassword", "newPassword")),
+ field("properties", object(
+ field("oldPassword", object(
+ field("type", "string"),
+ field("name", "Old Password"),
+ field("description", "Current password as a UTF-8 string."),
+ field("format", "password"))),
+ field("newPassword", object(
+ field("type", "string"),
+ field("name", "New Password"),
+ field("description", "New password as a UTF-8 string."),
+ field("format", "password")))))));
+ return Schema.schema().schema(jsonSchema).build();
+ }
+
+ private static org.forgerock.api.models.Action resetPasswordAction() {
+ return org.forgerock.api.models.Action.action()
+ .stability(EVOLVING)
+ .name("resetPassword")
+ .response(resetPasswordResponse())
+ .description("Reset a user password to a generated value. This action requires HTTPS.")
+ .error(errorRef(ERROR_BAD_REQUEST))
+ .error(errorRef(ERROR_UNAUTHORIZED))
+ .error(errorRef(ERROR_PASSWORD_MODIFY_REQUIRES_HTTPS))
+ .error(errorRef(ERROR_PASSWORD_MODIFY_REQUIRES_AUTHENTICATION))
+ .error(errorRef(ERROR_FORBIDDEN))
+ .error(errorRef(ERROR_NOT_FOUND))
+ .error(errorRef(ERROR_REQUEST_TIMEOUT))
+ .error(errorRef(ERROR_VERSION_MISMATCH))
+ .error(errorRef(ERROR_REQUEST_ENTITY_TOO_LARGE))
+ .error(errorRef(ERROR_READ_FOUND_MULTIPLE_ENTRIES))
+ .error(errorRef(ERROR_ADMIN_LIMIT_EXCEEDED))
+ .error(errorRef(ERROR_INTERNAL_SERVER_ERROR))
+ .error(errorRef(ERROR_UNAVAILABLE))
+ .build();
+ }
+
+ private static org.forgerock.api.models.Schema resetPasswordResponse() {
+ final JsonValue jsonSchema = json(object(
+ field("type", "object"),
+ field("properties", object(
+ field("generatedPassword", object(
+ field("type", "string"),
+ field("description", "Generated password to communicate to the user.")))))));
+ return Schema.schema().schema(jsonSchema).build();
+ }
+
+ private Schema buildJsonSchema() {
+ final List<String> requiredFields = new ArrayList<>();
+ JsonValue properties = json(JsonValue.object());
+ for (Map.Entry<String, PropertyMapper> prop : allProperties.entrySet()) {
+ final String propertyName = prop.getKey();
+ final PropertyMapper mapper = prop.getValue();
+ if (mapper.isRequired()) {
+ requiredFields.add(propertyName);
+ }
+ final JsonValue jsonSchema = mapper.toJsonSchema();
+ if (jsonSchema != null) {
+ properties.put(propertyName, jsonSchema);
+ }
+ }
+
+ final JsonValue jsonSchema = json(object(field("type", "object")));
+ if (!requiredFields.isEmpty()) {
+ jsonSchema.put("required", requiredFields);
+ }
+ if (properties.size() > 0) {
+ jsonSchema.put("properties", properties);
+ }
+ return Schema.schema().schema(jsonSchema).build();
+ }
+
+ private Errors errors() {
+ return Errors
+ .errors()
+ .put("passwordModifyRequiresHttps",
+ error(FORBIDDEN, "Password modify requires a secure connection."))
+ .put("passwordModifyRequiresAuthn",
+ error(FORBIDDEN, "Password modify requires user to be authenticated."))
+ .put("readFoundMultipleEntries",
+ error(INTERNAL_ERROR, "Multiple entries where found when trying to read a single entry."))
+ .put("adminLimitExceeded",
+ error(INTERNAL_ERROR, "The request exceeded an administrative limit."))
+ .build();
+ }
+
+ static ApiError error(int code, String description) {
+ return ApiError.apiError().code(code).description(description).build();
+ }
+
+ static ApiError error(int code, LocalizableString description) {
+ return ApiError.apiError().code(code).description(description).build();
+ }
+
+ static ApiError errorRef(String referenceValue) {
+ return ApiError.apiError().reference(ref(referenceValue)).build();
+ }
+
+ static org.forgerock.api.models.Resource resourceRef(String referenceValue) {
+ return org.forgerock.api.models.Resource.resource().reference(ref(referenceValue)).build();
+ }
+
+ static org.forgerock.api.models.Schema schemaRef(String referenceValue) {
+ return Schema.schema().reference(ref(referenceValue)).build();
+ }
+
+ static Reference ref(String referenceValue) {
+ return Reference.reference().value(referenceValue).build();
+ }
}
--
Gitblit v1.10.0