opendj-cli/src/main/java/com/forgerock/opendj/cli/ToolVersionHandler.java
@@ -15,12 +15,7 @@ */ package com.forgerock.opendj.cli; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.Enumeration; import java.util.jar.Attributes; import java.util.jar.Manifest; import com.forgerock.opendj.util.ManifestUtil; /** Class that prints the version of the SDK to System.out. */ public final class ToolVersionHandler implements VersionHandler { @@ -64,20 +59,6 @@ } private String getVersion() { try { final Enumeration<URL> manifests = getClass().getClassLoader().getResources("META-INF/MANIFEST.MF"); while (manifests.hasMoreElements()) { final URL manifestUrl = manifests.nextElement(); if (manifestUrl.toString().contains(moduleName)) { try (InputStream manifestStream = manifestUrl.openStream()) { final Attributes attrs = new Manifest(manifestStream).getMainAttributes(); return attrs.getValue("Bundle-Version") + " (revision " + attrs.getValue("SCM-Revision") + ")"; } } } return null; } catch (IOException e) { throw new RuntimeException("IOException while determining opendj tool version", e); } return ManifestUtil.getVersionWithRevision(moduleName); } } opendj-core/src/main/java/com/forgerock/opendj/util/ManifestUtil.java
New file @@ -0,0 +1,87 @@ /* * The contents of this file are subject to the terms of the Common Development and * Distribution License (the License). You may not use this file except in compliance with the * License. * * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the * specific language governing permission and limitations under the License. * * When distributing Covered Software, include this CDDL Header Notice in each file and include * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions Copyright [year] [name of copyright owner]". * * Copyright 2016 ForgeRock AS. */ package com.forgerock.opendj.util; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.Enumeration; import java.util.jar.Attributes; import java.util.jar.Manifest; import org.forgerock.util.Pair; /** Utility methods reading information from {@code opendj-core}'s manifest. */ public final class ManifestUtil { private static final String OPENDJ_CORE_VERSION; private static final String OPENDJ_CORE_VERSION_WITH_REVISION; static { final Pair<String, String> versions = getVersions("opendj-core"); OPENDJ_CORE_VERSION = versions.getFirst(); OPENDJ_CORE_VERSION_WITH_REVISION = versions.getSecond(); } /** * Returns the version with the revision contained in the module manifest whose name is provided. * * @param moduleName The module name for which to retrieve the version number * @return the version with the revision contained in the module manifest whose name is provided. */ public static String getVersionWithRevision(String moduleName) { if ("opendj-core".equals(moduleName)) { return OPENDJ_CORE_VERSION_WITH_REVISION; } return getVersions(moduleName).getSecond(); } /** * Returns the bundle version contained in the module manifest whose name is provided. * * @param moduleName The module name for which to retrieve the version number * @return the bundle version contained in the module manifest whose name is provided. */ public static String getBundleVersion(String moduleName) { if ("opendj-core".equals(moduleName)) { return OPENDJ_CORE_VERSION; } return getVersions(moduleName).getFirst(); } private static Pair<String, String> getVersions(String moduleName) { try { final Enumeration<URL> manifests = ManifestUtil.class.getClassLoader().getResources("META-INF/MANIFEST.MF"); while (manifests.hasMoreElements()) { final URL manifestUrl = manifests.nextElement(); if (manifestUrl.toString().contains(moduleName)) { try (InputStream manifestStream = manifestUrl.openStream()) { final Attributes attrs = new Manifest(manifestStream).getMainAttributes(); final String bundleVersion = attrs.getValue("Bundle-Version"); return Pair.of(bundleVersion, bundleVersion + " (revision " + attrs.getValue("SCM-Revision") + ")"); } } } return null; } catch (IOException e) { throw new RuntimeException("IOException while determining opendj tool version", e); } } private ManifestUtil() { // do not instantiate util classes } } opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLdapPropertyMapper.java
@@ -18,6 +18,8 @@ import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.*; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static org.forgerock.api.enums.WritePolicy.WRITE_ON_CREATE; import static org.forgerock.opendj.ldap.Attributes.emptyAttribute; import static org.forgerock.opendj.rest2ldap.Rest2Ldap.asResourceException; import static org.forgerock.opendj.rest2ldap.Utils.isNullOrEmpty; @@ -85,6 +87,16 @@ return getThis(); } @Override boolean isRequired() { return isRequired; } @Override boolean isMultiValued() { return isMultiValued; } /** * Indicates whether the LDAP attribute supports updates. The default is {@link WritabilityPolicy#READ_WRITE}. * @@ -351,4 +363,24 @@ } } void putWritabilityProperties(JsonValue jsonSchema) { switch (writabilityPolicy != null ? writabilityPolicy : WritabilityPolicy.READ_WRITE) { case CREATE_ONLY: jsonSchema.put("writePolicy", WRITE_ON_CREATE.toString()); jsonSchema.put("errorOnWritePolicyFailure", true); break; case CREATE_ONLY_DISCARD_WRITES: jsonSchema.put("writePolicy", WRITE_ON_CREATE.toString()); break; case READ_ONLY: jsonSchema.put("readOnly", true); jsonSchema.put("errorOnWritePolicyFailure", true); break; case READ_ONLY_DISCARD_WRITES: jsonSchema.put("readOnly", true); break; default: break; } } } opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractRequestHandler.java
@@ -15,6 +15,8 @@ */ package org.forgerock.opendj.rest2ldap; import org.forgerock.api.models.ApiDescription; import org.forgerock.http.ApiProducer; import org.forgerock.json.resource.ActionRequest; import org.forgerock.json.resource.ActionResponse; import org.forgerock.json.resource.CreateRequest; @@ -31,13 +33,14 @@ import org.forgerock.json.resource.ResourceResponse; import org.forgerock.json.resource.UpdateRequest; import org.forgerock.services.context.Context; import org.forgerock.services.descriptor.Describable; import org.forgerock.util.promise.Promise; /** * An abstract base class from which request handlers may be easily implemented. The default implementation of each * method is to invoke the {@link #handleRequest(Context, Request)} method. */ public abstract class AbstractRequestHandler implements RequestHandler { public abstract class AbstractRequestHandler implements RequestHandler, Describable<ApiDescription, Request> { /** Creates a new {@code AbstractRequestHandler}. */ protected AbstractRequestHandler() { // Nothing to do. @@ -96,4 +99,28 @@ protected <V> Promise<V, ResourceException> handleRequest(final Context context, final Request request) { return new NotSupportedException().asPromise(); } @Override public ApiDescription api(ApiProducer<ApiDescription> producer) { // api descriptions that are null will be ignored return null; } @Override public ApiDescription handleApiRequest(Context context, Request request) { // api requests are handled at a higher level by org.forgerock.opendj.rest2ldap.DescribableRequestHandler. // So this code is never reached. throw new UnsupportedOperationException("This should be handled by " + "org.forgerock.opendj.rest2ldap.DescribableRequestHandler.handleApiRequest()"); } @Override public void addDescriptorListener(Describable.Listener listener) { // nothing to do } @Override public void removeDescriptorListener(Describable.Listener listener) { // nothing to do } } opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/DescribableRequestHandler.java
New file @@ -0,0 +1,132 @@ /* * The contents of this file are subject to the terms of the Common Development and * Distribution License (the License). You may not use this file except in compliance with the * License. * * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the * specific language governing permission and limitations under the License. * * When distributing Covered Software, include this CDDL Header Notice in each file and include * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions Copyright [year] [name of copyright owner]". * * Copyright 2016 ForgeRock AS. */ package org.forgerock.opendj.rest2ldap; import org.forgerock.api.models.ApiDescription; import org.forgerock.http.ApiProducer; import org.forgerock.json.resource.ActionRequest; import org.forgerock.json.resource.ActionResponse; import org.forgerock.json.resource.CreateRequest; import org.forgerock.json.resource.DeleteRequest; import org.forgerock.json.resource.PatchRequest; import org.forgerock.json.resource.QueryRequest; import org.forgerock.json.resource.QueryResourceHandler; import org.forgerock.json.resource.QueryResponse; import org.forgerock.json.resource.ReadRequest; import org.forgerock.json.resource.Request; import org.forgerock.json.resource.RequestHandler; import org.forgerock.json.resource.ResourceException; import org.forgerock.json.resource.ResourceResponse; import org.forgerock.json.resource.UpdateRequest; import org.forgerock.services.context.Context; import org.forgerock.services.descriptor.Describable; import org.forgerock.util.Reject; import org.forgerock.util.promise.Promise; /** Decorator for a request handler that can return an api descriptor of the underlying handler. */ public class DescribableRequestHandler implements RequestHandler, Describable<ApiDescription, Request> { private final RequestHandler delegate; private final Describable<ApiDescription, Request> describableDelegate; private ApiDescription api; /** * Builds an object decorating the provided handler. * * @param handler * the handler to decorate. */ @SuppressWarnings("unchecked") public DescribableRequestHandler(final RequestHandler handler) { this.delegate = Reject.checkNotNull(handler); this.describableDelegate = delegate instanceof Describable ? (Describable<ApiDescription, Request>) delegate : null; } @Override public Promise<ActionResponse, ResourceException> handleAction(Context context, ActionRequest request) { return delegate.handleAction(wrap(context), request); } @Override public Promise<ResourceResponse, ResourceException> handleCreate(Context context, CreateRequest request) { return delegate.handleCreate(wrap(context), request); } @Override public Promise<ResourceResponse, ResourceException> handleDelete(Context context, DeleteRequest request) { return delegate.handleDelete(wrap(context), request); } @Override public Promise<ResourceResponse, ResourceException> handlePatch(Context context, PatchRequest request) { return delegate.handlePatch(wrap(context), request); } @Override public Promise<QueryResponse, ResourceException> handleQuery( Context context, QueryRequest request, QueryResourceHandler handler) { return delegate.handleQuery(wrap(context), request, handler); } @Override public Promise<ResourceResponse, ResourceException> handleRead(Context context, ReadRequest request) { return delegate.handleRead(wrap(context), request); } @Override public Promise<ResourceResponse, ResourceException> handleUpdate(Context context, UpdateRequest request) { return delegate.handleUpdate(wrap(context), request); } /** * Allows sub classes to wrap the provided context and return the wrapping context. * * @param context * the context to wrap * @return the wrapping context that should be used */ protected Context wrap(final Context context) { return context; } @Override public ApiDescription api(ApiProducer<ApiDescription> producer) { if (describableDelegate != null) { api = describableDelegate.api(producer); } return api; } @Override public ApiDescription handleApiRequest(Context context, Request request) { return api; } @Override public void addDescriptorListener(Describable.Listener listener) { if (describableDelegate != null) { describableDelegate.addDescriptorListener(listener); } } @Override public void removeDescriptorListener(Describable.Listener listener) { if (describableDelegate != null) { describableDelegate.removeDescriptorListener(listener); } } } opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JsonConstantPropertyMapper.java
@@ -16,6 +16,7 @@ package org.forgerock.opendj.rest2ldap; import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.*; import static org.forgerock.json.JsonValue.*; import static org.forgerock.opendj.ldap.Filter.alwaysFalse; import static org.forgerock.opendj.ldap.Filter.alwaysTrue; import static org.forgerock.opendj.rest2ldap.Utils.isNullOrEmpty; @@ -50,6 +51,16 @@ } @Override boolean isRequired() { return false; } @Override boolean isMultiValued() { return false; } @Override public String toString() { return "constant(" + value + ")"; } @@ -75,39 +86,38 @@ final JsonPointer path, final JsonPointer subPath, final FilterType type, final String operator, final Object valueAssertion) { final Filter filter; return newResultPromise(getLdapFilter0(subPath, type, valueAssertion)); } private Filter getLdapFilter0(final JsonPointer subPath, final FilterType type, final Object valueAssertion) { final JsonValue subValue = value.get(subPath); if (subValue == null) { filter = alwaysFalse(); return alwaysFalse(); } else if (type == FilterType.PRESENT) { filter = alwaysTrue(); return alwaysTrue(); } else if (value.isString() && valueAssertion instanceof String) { final String v1 = toLowerCase(value.asString()); final String v2 = toLowerCase((String) valueAssertion); switch (type) { case CONTAINS: filter = toFilter(v1.contains(v2)); break; return toFilter(v1.contains(v2)); case STARTS_WITH: filter = toFilter(v1.startsWith(v2)); break; return toFilter(v1.startsWith(v2)); default: filter = compare(type, v1, v2); break; return compare(type, v1, v2); } } else if (value.isNumber() && valueAssertion instanceof Number) { final Double v1 = value.asDouble(); final Double v2 = ((Number) valueAssertion).doubleValue(); filter = compare(type, v1, v2); return compare(type, v1, v2); } else if (value.isBoolean() && valueAssertion instanceof Boolean) { final Boolean v1 = value.asBoolean(); final Boolean v2 = (Boolean) valueAssertion; filter = compare(type, v1, v2); return compare(type, v1, v2); } else { // This property mapper is a candidate but it does not match. filter = alwaysFalse(); return alwaysFalse(); } return newResultPromise(filter); } @Override @@ -148,4 +158,43 @@ return alwaysFalse(); // Not supported. } } @Override JsonValue toJsonSchema() { return toJsonSchema(value); } private static JsonValue toJsonSchema(JsonValue value) { if (value.isMap()) { final JsonValue jsonSchema = json(object(field("type", "object"))); final JsonValue jsonProps = json(object()); for (String key : value.keys()) { jsonProps.put(key, toJsonSchema(value.get(key))); } jsonSchema.put("properties", jsonSchema); return jsonSchema; } else if (value.isCollection()) { final JsonValue jsonSchema = json(object(field("type", "array"))); final JsonValue firstItem = value.get(value.keys().iterator().next()); // assume all items have the same schema jsonSchema.put("items", toJsonSchema(firstItem)); if (value.isSet()) { jsonSchema.put("uniqueItems", true); } return jsonSchema; } else if (value.isBoolean()) { return json(object(field("type", "boolean"), field("default", value))); } else if (value.isString()) { return json(object(field("type", "string"), field("default", value))); } else if (value.isNumber()) { return json(object(field("type", "number"), field("default", value))); } else if (value.isNull()) { return json(object(field("type", "null"))); } else { return null; } } } opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectPropertyMapper.java
@@ -17,6 +17,7 @@ import static org.forgerock.opendj.rest2ldap.Rest2Ldap.simple; import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.*; import static org.forgerock.json.JsonValue.*; import static org.forgerock.json.resource.PatchOperation.operation; import static org.forgerock.opendj.ldap.Filter.alwaysFalse; import static org.forgerock.opendj.rest2ldap.Rest2Ldap.asResourceException; @@ -75,11 +76,21 @@ // Nothing to do. } @Override boolean isRequired() { return false; } @Override boolean isMultiValued() { return false; } /** * Creates an explicit mapping for a property contained in the JSON object. When user attributes are * {@link #includeAllUserAttributesByDefault included} by default, be careful to {@link * #excludedDefaultUserAttributes exclude} any attributes which have explicit mappings defined using this method, * otherwise they will be duplicated in the JSON representation. * {@link #includeAllUserAttributesByDefault(boolean) included} by default, be careful to {@link * #excludedDefaultUserAttributes(Collection) exclude} any attributes which have explicit mappings defined using * this method, otherwise they will be duplicated in the JSON representation. * * @param name * The name of the JSON property to be mapped. @@ -94,8 +105,8 @@ /** * Specifies whether all LDAP user attributes should be mapped by default using the default schema based mapping * rules. Individual attributes can be excluded using {@link #excludedDefaultUserAttributes} in order to prevent * attributes with explicit mappings being mapped twice. * rules. Individual attributes can be excluded using {@link #excludedDefaultUserAttributes(Collection)} in order * to prevent attributes with explicit mappings being mapped twice. * * @param include {@code true} if all LDAP user attributes be mapped by default. * @return A reference to this property mapper. @@ -107,8 +118,8 @@ /** * Specifies zero or more user attributes which will be excluded from the default user attribute mappings when * enabled using {@link #includeAllUserAttributesByDefault}. Attributes which have explicit mappings should be * excluded in order to prevent duplication. * enabled using {@link #includeAllUserAttributesByDefault(boolean)}. Attributes which have explicit mappings * should be excluded in order to prevent duplication. * * @param attributeNames The list of attributes to be excluded. * @return A reference to this property mapper. @@ -119,8 +130,8 @@ /** * Specifies zero or more user attributes which will be excluded from the default user attribute mappings when * enabled using {@link #includeAllUserAttributesByDefault}. Attributes which have explicit mappings should be * excluded in order to prevent duplication. * enabled using {@link #includeAllUserAttributesByDefault(boolean)}. Attributes which have explicit mappings * should be excluded in order to prevent duplication. * * @param attributeNames The list of attributes to be excluded. * @return A reference to this property mapper. @@ -427,4 +438,27 @@ return includeAllUserAttributesByDefault && (excludedDefaultUserAttributes.isEmpty() || !excludedDefaultUserAttributes.contains(attributeName)); } @Override JsonValue toJsonSchema() { final List<String> requiredFields = new ArrayList<>(); final JsonValue jsonProps = json(object()); for (Mapping mapping : mappings.values()) { final String attribute = mapping.name; PropertyMapper mapper = mapping.mapper; jsonProps.put(attribute, mapper.toJsonSchema()); if (mapper.isRequired()) { requiredFields.add(attribute); } } final JsonValue jsonSchema = json(object(field("type", "object"))); if (!requiredFields.isEmpty()) { jsonSchema.put("required", requiredFields); } if (jsonProps.size() > 0) { jsonSchema.put("properties", jsonProps); } return jsonSchema; } } opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/PropertyMapper.java
@@ -40,6 +40,10 @@ // Nothing to do. } abstract boolean isRequired(); abstract boolean isMultiValued(); /** * Maps a JSON value to one or more LDAP attributes, returning a promise * once the transformation has completed. This method is invoked when a REST @@ -193,4 +197,6 @@ // TODO: methods for obtaining schema information (e.g. name, description, type information). // TODO: methods for creating sort controls. abstract JsonValue toJsonSchema(); } opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferencePropertyMapper.java
@@ -15,14 +15,15 @@ */ package org.forgerock.opendj.rest2ldap; import static org.forgerock.json.JsonValue.*; import static org.forgerock.opendj.ldap.ResultCode.ADMIN_LIMIT_EXCEEDED; import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.*; import static org.forgerock.opendj.ldap.LdapException.newLdapException; import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest; import static org.forgerock.opendj.rest2ldap.Rest2Ldap.asResourceException; import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.*; import static org.forgerock.opendj.rest2ldap.Utils.connectionFrom; import static org.forgerock.util.Reject.checkNotNull; import static org.forgerock.opendj.rest2ldap.Utils.newBadRequestException; import static org.forgerock.util.Reject.checkNotNull; import static org.forgerock.util.promise.Promises.newResultPromise; import java.util.ArrayList; @@ -67,9 +68,7 @@ * valued LDAP attribute. */ public final class ReferencePropertyMapper extends AbstractLdapPropertyMapper<ReferencePropertyMapper> { /** * The maximum number of candidate references to allow in search filters. */ /** The maximum number of candidate references to allow in search filters. */ private static final int SEARCH_MAX_CANDIDATES = 1000; private final DnTemplate baseDnTemplate; @@ -358,4 +357,16 @@ } }); } @Override JsonValue toJsonSchema() { if (mapper.isMultiValued()) { final JsonValue jsonSchema = json(object(field("type", "array"))); jsonSchema.put("items", mapper.toJsonSchema()); jsonSchema.put("uniqueItems", true); putWritabilityProperties(jsonSchema); return jsonSchema; } return mapper.toJsonSchema(); } } 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(); } } opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ResourceTypePropertyMapper.java
@@ -17,6 +17,8 @@ package org.forgerock.opendj.rest2ldap; import static java.util.Collections.singletonList; import static org.forgerock.json.JsonValue.*; import static org.forgerock.opendj.ldap.Filter.alwaysFalse; import static org.forgerock.opendj.ldap.Filter.alwaysTrue; import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_ILLEGAL_FILTER_ASSERTION_VALUE; @@ -57,6 +59,16 @@ } @Override boolean isRequired() { return false; } @Override boolean isMultiValued() { return false; } @Override Promise<List<Attribute>, ResourceException> create(final Context context, final Resource resource, final JsonPointer path, final JsonValue v) { @@ -120,4 +132,9 @@ return newResultPromise(Collections.<Modification>emptyList()); } } @Override JsonValue toJsonSchema() { return json(object(field("type", "string"))); } } opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2Ldap.java
@@ -26,28 +26,17 @@ import java.util.LinkedHashMap; import java.util.Map; import org.forgerock.json.resource.ActionRequest; import org.forgerock.json.resource.ActionResponse; import org.forgerock.json.resource.BadRequestException; import org.forgerock.json.resource.CreateRequest; import org.forgerock.json.resource.DeleteRequest; import org.forgerock.json.resource.ForbiddenException; import org.forgerock.json.resource.InternalServerErrorException; import org.forgerock.json.resource.NotFoundException; import org.forgerock.json.resource.PatchRequest; import org.forgerock.json.resource.PermanentException; import org.forgerock.json.resource.PreconditionFailedException; import org.forgerock.json.resource.QueryRequest; import org.forgerock.json.resource.QueryResourceHandler; import org.forgerock.json.resource.QueryResponse; import org.forgerock.json.resource.ReadRequest; import org.forgerock.json.resource.RequestHandler; import org.forgerock.json.resource.ResourceException; import org.forgerock.json.resource.ResourceResponse; import org.forgerock.json.resource.RetryableException; import org.forgerock.json.resource.Router; import org.forgerock.json.resource.ServiceUnavailableException; import org.forgerock.json.resource.UpdateRequest; import org.forgerock.opendj.ldap.AssertionFailureException; import org.forgerock.opendj.ldap.AttributeDescription; import org.forgerock.opendj.ldap.AuthenticationException; @@ -65,7 +54,6 @@ import org.forgerock.util.Option; import org.forgerock.util.Options; import org.forgerock.util.Reject; import org.forgerock.util.promise.Promise; /** * Provides methods for constructing Rest2Ldap protocol gateways. Applications construct a new Rest2Ldap @@ -381,44 +369,9 @@ } private RequestHandler rest2LdapContext(final RequestHandler delegate) { return new RequestHandler() { public Promise<ActionResponse, ResourceException> handleAction(final Context context, final ActionRequest request) { return delegate.handleAction(wrap(context), request); } public Promise<ResourceResponse, ResourceException> handleCreate(final Context context, final CreateRequest request) { return delegate.handleCreate(wrap(context), request); } public Promise<ResourceResponse, ResourceException> handleDelete(final Context context, final DeleteRequest request) { return delegate.handleDelete(wrap(context), request); } public Promise<ResourceResponse, ResourceException> handlePatch(final Context context, final PatchRequest request) { return delegate.handlePatch(wrap(context), request); } public Promise<QueryResponse, ResourceException> handleQuery(final Context context, final QueryRequest request, final QueryResourceHandler handler) { return delegate.handleQuery(wrap(context), request, handler); } public Promise<ResourceResponse, ResourceException> handleRead(final Context context, final ReadRequest request) { return delegate.handleRead(wrap(context), request); } public Promise<ResourceResponse, ResourceException> handleUpdate(final Context context, final UpdateRequest request) { return delegate.handleUpdate(wrap(context), request); } private Context wrap(final Context context) { return new DescribableRequestHandler(delegate) { @Override protected Context wrap(final Context context) { return new Rest2LdapContext(context, Rest2Ldap.this); } }; opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LdapHttpApplication.java
@@ -22,7 +22,6 @@ import static org.forgerock.json.JsonValueFunctions.duration; import static org.forgerock.json.JsonValueFunctions.enumConstant; import static org.forgerock.json.JsonValueFunctions.setOf; import static org.forgerock.json.resource.http.CrestHttp.newHttpHandler; import static org.forgerock.opendj.ldap.KeyManagers.useSingleCertificate; import static org.forgerock.opendj.rest2ldap.Rest2LdapJsonConfigurator.*; import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.*; @@ -69,7 +68,10 @@ import org.forgerock.i18n.LocalizedIllegalArgumentException; import org.forgerock.i18n.slf4j.LocalizedLogger; import org.forgerock.json.JsonValue; import org.forgerock.json.resource.CrestApplication; import org.forgerock.json.resource.RequestHandler; import org.forgerock.json.resource.Resources; import org.forgerock.json.resource.http.CrestHttp; import org.forgerock.opendj.ldap.Connection; import org.forgerock.opendj.ldap.ConnectionFactory; import org.forgerock.opendj.ldap.DN; @@ -94,6 +96,8 @@ import org.forgerock.util.time.Duration; import org.forgerock.util.time.TimeService; import com.forgerock.opendj.util.ManifestUtil; /** Rest2ldap HTTP application. */ public class Rest2LdapHttpApplication implements HttpApplication { private static final String DEFAULT_ROOT_FACTORY = "root"; @@ -231,6 +235,27 @@ return configureEndpoints(endpointsDirectory, options); } private static Handler newHttpHandler(final RequestHandler requestHandler) { final org.forgerock.json.resource.ConnectionFactory factory = Resources.newInternalConnectionFactory(requestHandler); return CrestHttp.newHttpHandler(new CrestApplication() { @Override public org.forgerock.json.resource.ConnectionFactory getConnectionFactory() { return factory; } @Override public String getApiId() { return "frapi:opendj:rest2ldap"; } @Override public String getApiVersion() { return ManifestUtil.getVersionWithRevision("opendj-core"); } }); } private void configureSecurity(final JsonValue configuration) { trustManager = configureTrustManager(configuration); keyManager = configureKeyManager(configuration); opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimplePropertyMapper.java
@@ -30,6 +30,8 @@ import org.forgerock.opendj.ldap.ByteString; import org.forgerock.opendj.ldap.Entry; import org.forgerock.opendj.ldap.Filter; import org.forgerock.opendj.ldap.schema.AttributeType; import org.forgerock.opendj.ldap.schema.CoreSchema; import org.forgerock.services.context.Context; import org.forgerock.util.Function; import org.forgerock.util.promise.NeverThrowsException; @@ -37,6 +39,7 @@ import static java.util.Collections.*; import static org.forgerock.json.JsonValue.*; import static org.forgerock.opendj.ldap.Filter.*; import static org.forgerock.opendj.rest2ldap.Rest2Ldap.asResourceException; import static org.forgerock.opendj.rest2ldap.Utils.*; @@ -198,4 +201,39 @@ return encoder == null ? jsonToByteString(ldapAttributeName) : encoder; } @Override JsonValue toJsonSchema() { final AttributeType attrType = ldapAttributeName.getAttributeType(); final JsonValue jsonSchema = json(object(field("type", toJsonSchemaType(attrType)))); final String description = attrType.getDescription(); if (description != null && !"".equals(description)) { jsonSchema.put("title", description); } putWritabilityProperties(jsonSchema); return jsonSchema; } private static String toJsonSchemaType(AttributeType attrType) { if (attrType.isPlaceHolder()) { return "string"; } // TODO JNR cannot use switch + SchemaConstants.SYNTAX_DIRECTORY_STRING_OID // because the class is not public // this is not nice :( // TODO JNR not so sure about these mappings final String oid = attrType.getSyntax().getOID(); if (CoreSchema.getDirectoryStringSyntax().getOID().equals(oid) || CoreSchema.getOctetStringSyntax().getOID().equals(oid)) { return "string"; } else if (CoreSchema.getBooleanSyntax().getOID().equals(oid)) { return "boolean"; } else if (CoreSchema.getIntegerSyntax().getOID().equals(oid)) { return "integer"; } else if (CoreSchema.getNumericStringSyntax().getOID().equals(oid)) { return "number"; } return "string"; } } opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResource.java
@@ -18,6 +18,8 @@ import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_UNRECOGNIZED_SUB_RESOURCE_TYPE; import org.forgerock.api.models.ApiDescription; import org.forgerock.http.ApiProducer; import org.forgerock.i18n.LocalizableMessage; import org.forgerock.i18n.LocalizedIllegalArgumentException; import org.forgerock.json.resource.ActionRequest; @@ -213,5 +215,10 @@ } }); } @Override public ApiDescription api(ApiProducer<ApiDescription> producer) { return resource.subResourcesApi(producer); } } } opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceCollection.java
@@ -28,6 +28,8 @@ import static org.forgerock.opendj.rest2ldap.Utils.newBadRequestException; import static org.forgerock.util.promise.Promises.newResultPromise; import org.forgerock.api.models.ApiDescription; import org.forgerock.http.ApiProducer; import org.forgerock.http.routing.UriRouterContext; import org.forgerock.i18n.LocalizedIllegalArgumentException; import org.forgerock.json.resource.ActionRequest; @@ -378,6 +380,11 @@ protected <V> Promise<V, ResourceException> handleRequest(final Context context, final Request request) { return new BadRequestException(ERR_UNSUPPORTED_REQUEST_AGAINST_COLLECTION.get().toString()).asPromise(); } @Override public ApiDescription api(ApiProducer<ApiDescription> producer) { return resource.collectionApi(isReadOnly); } } /** @@ -444,5 +451,17 @@ private <T> Function<ResourceException, T, ResourceException> convert404To400() { return SubResource.convert404To400(ERR_UNSUPPORTED_REQUEST_AGAINST_INSTANCE.get()); } /** * Returns {@code null} because the corresponding {@link ApiDescription} * is returned by the {@link CollectionHandler#api(ApiProducer)} method. * <p> * This avoids problems when trying to {@link ApiProducer#merge(java.util.List) merge} * {@link ApiDescription}s with the same path. */ @Override public ApiDescription api(ApiProducer<ApiDescription> producer) { return null; } } } opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceSingleton.java
@@ -25,6 +25,8 @@ import static org.forgerock.opendj.rest2ldap.RoutingContext.newRoutingContext; import static org.forgerock.util.promise.Promises.newResultPromise; import org.forgerock.api.models.ApiDescription; import org.forgerock.http.ApiProducer; import org.forgerock.json.resource.ActionRequest; import org.forgerock.json.resource.ActionResponse; import org.forgerock.json.resource.BadRequestException; @@ -203,5 +205,10 @@ private <T> Function<ResourceException, T, ResourceException> convert404To400() { return SubResource.convert404To400(ERR_UNSUPPORTED_REQUEST_AGAINST_SINGLETON.get()); } @Override public ApiDescription api(ApiProducer<ApiDescription> producer) { return getResource().instanceApi(isReadOnly); } } } opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfiguratorTest.java
New file @@ -0,0 +1,109 @@ /* * The contents of this file are subject to the terms of the Common Development and * Distribution License (the License). You may not use this file except in compliance with the * License. * * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the * specific language governing permission and limitations under the License. * * When distributing Covered Software, include this CDDL Header Notice in each file and include * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions Copyright [year] [name of copyright owner]". * * Copyright 2016 ForgeRock AS. */ package org.forgerock.opendj.rest2ldap; import static org.assertj.core.api.Assertions.*; import static org.forgerock.http.util.Json.*; import static org.forgerock.json.resource.Requests.*; import static org.forgerock.json.resource.ResourcePath.*; import static org.forgerock.opendj.rest2ldap.Rest2Ldap.*; import static org.forgerock.util.Options.*; import java.io.File; import java.io.StringReader; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Collections; import org.forgerock.api.CrestApiProducer; import org.forgerock.api.models.ApiDescription; import org.forgerock.http.routing.UriRouterContext; import org.forgerock.http.util.Json; import org.forgerock.json.JsonValue; import org.forgerock.json.resource.Request; import org.forgerock.json.resource.RequestHandler; import org.forgerock.services.context.Context; import org.forgerock.services.context.RootContext; import org.forgerock.testng.ForgeRockTestCase; import org.forgerock.util.Options; import org.testng.annotations.Test; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; /** * This class tests that the {@link Rest2LdapJsonConfigurator} class can successfully create its * model and generate its API description from the json configuration files. */ @Test @SuppressWarnings("javadoc") 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 CONFIG_DIR = Paths.get( "../opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/rest2ldap"); @Test public void testConfigureEndpointsWithApiDescription() throws Exception { final DescribableRequestHandler handler = configureEndpoints(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 parseJson(prettyPrint(api)); assertThat(api.getId()).isEqualTo(ID); assertThat(api.getVersion()).isEqualTo(VERSION); assertThat(api.getPaths().getNames()).containsOnly("/api/users", "/api/groups"); assertThat(api.getDefinitions().getNames()).containsOnly( "frapi:opendj:rest2ldap:group:1.0", "frapi:opendj:rest2ldap:user:1.0", "frapi:opendj:rest2ldap:posixUser:1.0"); } private DescribableRequestHandler configureEndpoints(final File endpointsDir) throws Exception { final RequestHandler rh = Rest2LdapJsonConfigurator.configureEndpoints(endpointsDir, Options.defaultOptions()); 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)); 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()); final ObjectWriter writer = objectMapper.writer().withDefaultPrettyPrinter(); return writer.writeValueAsString(o); } static JsonValue parseJson(final String json) throws Exception { try (StringReader r = new StringReader(json)) { return new JsonValue(readJsonLenient(r)); } } } opendj-server-legacy/src/main/java/org/opends/server/core/HttpEndpointConfigManager.java
@@ -157,7 +157,6 @@ return ccr; } final RouteMatcher<Request> route = newRoute(configuration.getBasePath()); try { final HttpApplication application = loadEndpoint(configuration).newHttpApplication(); @@ -170,13 +169,13 @@ { ccr.setResultCodeIfSuccess(DirectoryServer.getServerErrorResultCode()); ccr.addMessage(ERR_CONFIG_HTTPENDPOINT_UNABLE_TO_START.get(configuration.dn(), stackTraceToSingleLineString(e))); router.addRoute(route, ErrorHandler.INTERNAL_SERVER_ERROR); router.addRoute(newRoute(configuration.getBasePath()), ErrorHandler.INTERNAL_SERVER_ERROR); } catch (InitializationException | ConfigException ie) { ccr.setResultCodeIfSuccess(DirectoryServer.getServerErrorResultCode()); ccr.addMessage(ie.getMessageObject()); router.addRoute(route, ErrorHandler.INTERNAL_SERVER_ERROR); router.addRoute(newRoute(configuration.getBasePath()), ErrorHandler.INTERNAL_SERVER_ERROR); } return ccr; } opendj-server-legacy/src/main/java/org/opends/server/protocols/http/HTTPConnectionHandler.java
@@ -42,15 +42,17 @@ import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; import org.forgerock.http.ApiProducer; import org.forgerock.http.DescribedHttpApplication; import org.forgerock.http.Filter; import org.forgerock.http.Handler; import org.forgerock.http.HttpApplication; import org.forgerock.http.HttpApplicationException; import org.forgerock.http.handler.Handlers; import org.forgerock.http.io.Buffer; import org.forgerock.http.protocol.Request; import org.forgerock.http.protocol.Response; import org.forgerock.http.protocol.Status; import org.forgerock.http.swagger.SwaggerApiProducer; import org.forgerock.i18n.LocalizableMessage; import org.forgerock.i18n.slf4j.LocalizedLogger; import org.forgerock.opendj.config.server.ConfigChangeResult; @@ -97,6 +99,8 @@ import org.opends.server.util.SelectableCertificateKeyManager; import org.opends.server.util.StaticUtils; import io.swagger.models.Swagger; /** * This class defines a connection handler that will be used for communicating * with clients over HTTP. The connection handler is responsible for @@ -899,11 +903,11 @@ } /** * This is the root {@link HttpApplication} handling all the requests from the * {@link HTTPConnectionHandler}. If accepted, requests are audited and then * forwarded to the global {@link ServerContext#getHTTPRouter()}. * This is the root {@link org.forgerock.http.HttpApplication} handling all the requests from the * {@link HTTPConnectionHandler}. If accepted, requests are audited and then forwarded to the * global {@link ServerContext#getHTTPRouter()}. */ private final class RootHttpApplication implements HttpApplication private final class RootHttpApplication implements DescribedHttpApplication { @Override public Handler start() throws HttpApplicationException @@ -934,6 +938,13 @@ { return null; } @Override public ApiProducer<Swagger> getApiProducer() { // Needed to enforce generation of CREST APIs return new SwaggerApiProducer(null, null, null); } } /** Moves the processing of the request in this Directory Server's worker thread. */ opendj-server-legacy/src/main/java/org/opends/server/protocols/http/rest2ldap/Rest2LdapEndpoint.java
@@ -15,7 +15,6 @@ */ package org.opends.server.protocols.http.rest2ldap; import static org.forgerock.json.resource.http.CrestHttp.newHttpHandler; import static org.forgerock.opendj.rest2ldap.Rest2LdapJsonConfigurator.configureEndpoint; import static org.forgerock.util.Options.defaultOptions; import static org.opends.messages.ConfigMessages.ERR_CONFIG_REST2LDAP_INVALID; @@ -32,12 +31,18 @@ import org.forgerock.http.HttpApplicationException; import org.forgerock.http.io.Buffer; import org.forgerock.json.JsonValueException; import org.forgerock.json.resource.CrestApplication; import org.forgerock.json.resource.RequestHandler; import org.forgerock.json.resource.Resources; import org.forgerock.json.resource.http.CrestHttp; import org.forgerock.opendj.rest2ldap.DescribableRequestHandler; import org.forgerock.opendj.server.config.server.Rest2ldapEndpointCfg; import org.forgerock.util.Factory; import org.opends.server.api.HttpEndpoint; import org.opends.server.core.ServerContext; import org.opends.server.protocols.http.LocalizedHttpApplicationException; import org.opends.server.types.InitializationException; import org.opends.server.util.BuildVersion; /** * Encapsulates configuration required to start a REST2LDAP application embedded @@ -95,6 +100,27 @@ } } private Handler newHttpHandler(final RequestHandler requestHandler) { final DescribableRequestHandler handler = new DescribableRequestHandler(requestHandler); final org.forgerock.json.resource.ConnectionFactory factory = Resources.newInternalConnectionFactory(handler); return CrestHttp.newHttpHandler(new CrestApplication() { @Override public org.forgerock.json.resource.ConnectionFactory getConnectionFactory() { return factory; } @Override public String getApiId() { return "frapi:opendj:rest2ldap"; } @Override public String getApiVersion() { return BuildVersion.binaryVersion().toStringNoRevision(); } }); } @Override public void stop() { opendj-server-legacy/src/main/java/org/opends/server/util/BuildVersion.java
@@ -276,6 +276,17 @@ { return Utils.joinAsString(".", major, minor, point, rev); } return toStringNoRevision(); } /** * Returns a string representation of the BuildVersion including the major, minor and point * versions, but excluding the revision number. * * @return a string representation excluding the revision number */ public String toStringNoRevision() { return Utils.joinAsString(".", major, minor, point); }