From 8dd4c87972373e0eb1c074905819ae51187f98ad Mon Sep 17 00:00:00 2001
From: Matthew Swift <matthew.swift@forgerock.com>
Date: Fri, 21 Oct 2016 09:55:23 +0000
Subject: [PATCH] OPENDJ-3414 Support deep querying of LDAP attributes having JSON syntax
---
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfigurator.java | 5
opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/JsonPropertyMapperTest.java | 61 ++++++++++++
opendj-rest2ldap/src/main/resources/org/forgerock/opendj/rest2ldap/rest2ldap.properties | 4
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2Ldap.java | 22 ++++
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JsonPropertyMapper.java | 194 ++++++++++++++++++++++++++++++++++++++
5 files changed, 281 insertions(+), 5 deletions(-)
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JsonPropertyMapper.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JsonPropertyMapper.java
new file mode 100644
index 0000000..9ffd810
--- /dev/null
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JsonPropertyMapper.java
@@ -0,0 +1,194 @@
+/*
+ * 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 java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+import static org.forgerock.json.JsonValue.field;
+import static org.forgerock.json.JsonValue.object;
+import static org.forgerock.opendj.rest2ldap.Rest2Ldap.asResourceException;
+import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_ENCODING_VALUES_FOR_FIELD;
+import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_PATCH_JSON_INTERNAL_PROPERTY;
+import static org.forgerock.opendj.rest2ldap.Utils.jsonToAttribute;
+import static org.forgerock.opendj.rest2ldap.Utils.newBadRequestException;
+import static org.forgerock.opendj.rest2ldap.Utils.newNotSupportedException;
+import static org.forgerock.opendj.rest2ldap.schema.JsonSchema.byteStringToJson;
+import static org.forgerock.opendj.rest2ldap.schema.JsonSchema.jsonToByteString;
+import static org.forgerock.util.promise.Promises.newResultPromise;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
+import org.forgerock.json.JsonPointer;
+import org.forgerock.json.JsonValue;
+import org.forgerock.json.resource.PatchOperation;
+import org.forgerock.json.resource.ResourceException;
+import org.forgerock.opendj.ldap.Attribute;
+import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.Filter;
+import org.forgerock.opendj.ldap.Modification;
+import org.forgerock.services.context.Context;
+import org.forgerock.util.promise.Promise;
+import org.forgerock.util.query.QueryFilter;
+
+/** A property mapper which provides a mapping from a JSON value to an LDAP attribute having the JSON syntax. */
+public final class JsonPropertyMapper extends AbstractLdapPropertyMapper<JsonPropertyMapper> {
+ /**
+ * The default JSON schema for this property. According to json-schema.org the {} schema allows anything.
+ * However, the OpenAPI transformer seems to expect at least a "type" field to be present.
+ */
+ private static final JsonValue ANY_SCHEMA = new JsonValue(object(field("type", "object")));
+ private JsonValue jsonSchema = ANY_SCHEMA;
+
+ JsonPropertyMapper(final AttributeDescription ldapAttributeName) {
+ super(ldapAttributeName);
+ }
+
+ /**
+ * Sets the default JSON value which should be substituted when the LDAP attribute is not found in the LDAP entry.
+ *
+ * @param defaultValue
+ * The default JSON value.
+ * @return This property mapper.
+ */
+ public JsonPropertyMapper defaultJsonValue(final Object defaultValue) {
+ this.defaultJsonValues = defaultValue != null ? singletonList(defaultValue) : emptyList();
+ return this;
+ }
+
+ /**
+ * Sets the default JSON values which should be substituted when the LDAP attribute is not found in the LDAP entry.
+ *
+ * @param defaultValues
+ * The default JSON values.
+ * @return This property mapper.
+ */
+ public JsonPropertyMapper defaultJsonValues(final Collection<?> defaultValues) {
+ this.defaultJsonValues = defaultValues != null ? new ArrayList<>(defaultValues) : emptyList();
+ return this;
+ }
+
+ /**
+ * Sets the JSON schema corresponding to this simple property mapper. If not {@code null},
+ * it will be returned by {@link #toJsonSchema()}, otherwise a default JSON schema will be
+ * automatically generated with the information available in this property mapper.
+ *
+ * @param jsonSchema
+ * the JSON schema corresponding to this simple property mapper. Can be {@code null}
+ * @return This property mapper.
+ */
+ public JsonPropertyMapper jsonSchema(JsonValue jsonSchema) {
+ this.jsonSchema = jsonSchema != null ? jsonSchema : ANY_SCHEMA;
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return "json(" + ldapAttributeName + ")";
+ }
+
+ @Override
+ Promise<Filter, ResourceException> getLdapFilter(final Context context, final Resource resource,
+ final JsonPointer path, final JsonPointer subPath,
+ final FilterType type, final String operator,
+ final Object valueAssertion) {
+ final QueryFilter<JsonPointer> queryFilter = toQueryFilter(type, subPath, operator, valueAssertion);
+ return newResultPromise(Filter.equality(ldapAttributeName.toString(), queryFilter));
+ }
+
+ private QueryFilter<JsonPointer> toQueryFilter(final FilterType type, final JsonPointer subPath,
+ final String operator, final Object valueAssertion) {
+ switch (type) {
+ case CONTAINS:
+ return QueryFilter.contains(subPath, valueAssertion);
+ case STARTS_WITH:
+ return QueryFilter.startsWith(subPath, valueAssertion);
+ case EQUAL_TO:
+ return QueryFilter.equalTo(subPath, valueAssertion);
+ case GREATER_THAN:
+ return QueryFilter.greaterThan(subPath, valueAssertion);
+ case GREATER_THAN_OR_EQUAL_TO:
+ return QueryFilter.greaterThanOrEqualTo(subPath, valueAssertion);
+ case LESS_THAN:
+ return QueryFilter.lessThan(subPath, valueAssertion);
+ case LESS_THAN_OR_EQUAL_TO:
+ return QueryFilter.lessThanOrEqualTo(subPath, valueAssertion);
+ case PRESENT:
+ return QueryFilter.present(subPath);
+ case EXTENDED:
+ return QueryFilter.extendedMatch(subPath, operator, valueAssertion);
+ default:
+ return QueryFilter.alwaysFalse();
+ }
+ }
+
+ @Override
+ Promise<Attribute, ResourceException> getNewLdapAttributes(final Context context, final Resource resource,
+ final JsonPointer path, final List<Object> newValues) {
+ try {
+ return newResultPromise(jsonToAttribute(newValues, ldapAttributeName, jsonToByteString()));
+ } catch (final Exception e) {
+ return newBadRequestException(ERR_ENCODING_VALUES_FOR_FIELD.get(path, e.getMessage())).asPromise();
+ }
+ }
+
+ @Override
+ JsonPropertyMapper getThis() {
+ return this;
+ }
+
+ /** Intercept attempts to patch internal fields and reject these as unsupported rather than unrecognized. */
+ @Override
+ Promise<List<Modification>, ResourceException> patch(final Context context, final Resource resource,
+ final JsonPointer path, final PatchOperation operation) {
+ final JsonPointer field = operation.getField();
+ if (field.isEmpty() || field.size() == 1 && field.get(0).equals("-")) {
+ return super.patch(context, resource, path, operation);
+ }
+ return newNotSupportedException(ERR_PATCH_JSON_INTERNAL_PROPERTY.get(field, path, path)).asPromise();
+ }
+
+ @SuppressWarnings("fallthrough")
+ @Override
+ Promise<JsonValue, ResourceException> read(final Context context, final Resource resource, final JsonPointer path,
+ final Entry e) {
+ try {
+ final Set<Object> s = e.parseAttribute(ldapAttributeName).asSetOf(byteStringToJson(), defaultJsonValues);
+ switch (s.size()) {
+ case 0:
+ return newResultPromise(null);
+ case 1:
+ if (attributeIsSingleValued()) {
+ return newResultPromise(new JsonValue(s.iterator().next()));
+ }
+ // Fall-though: unexpectedly got multiple values. It's probably best to just return them.
+ default:
+ return newResultPromise(new JsonValue(new ArrayList<>(s)));
+ }
+ } catch (final Exception ex) {
+ // The LDAP attribute could not be decoded.
+ return asResourceException(ex).asPromise();
+ }
+ }
+
+ @Override
+ JsonValue toJsonSchema() {
+ return jsonSchema;
+ }
+}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2Ldap.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2Ldap.java
index ae42e86..114b0de 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2Ldap.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2Ldap.java
@@ -295,6 +295,28 @@
}
/**
+ * Returns a property mapper which provides a mapping from a JSON value to a LDAP attribute having the JSON syntax.
+ *
+ * @param attribute
+ * The LDAP attribute to be mapped.
+ * @return The property mapper.
+ */
+ public static JsonPropertyMapper json(final AttributeDescription attribute) {
+ return new JsonPropertyMapper(attribute);
+ }
+
+ /**
+ * Returns a property mapper which provides a mapping from a JSON value to a LDAP attribute having the JSON syntax.
+ *
+ * @param attribute
+ * The LDAP attribute to be mapped.
+ * @return The property mapper.
+ */
+ public static JsonPropertyMapper json(final String attribute) {
+ return json(AttributeDescription.valueOf(attribute));
+ }
+
+ /**
* Adapts a {@code Throwable} to a {@code ResourceException}. If the {@code Throwable} is an LDAP
* {@link LdapException} then an appropriate {@code ResourceException} is returned, otherwise an {@code
* InternalServerErrorException} is returned.
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 d902445..65572d5 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
@@ -79,7 +79,6 @@
import org.forgerock.opendj.ldap.SSLContextBuilder;
import org.forgerock.opendj.ldap.requests.BindRequest;
import org.forgerock.opendj.ldap.requests.Requests;
-import org.forgerock.opendj.rest2ldap.schema.JsonSchema;
import org.forgerock.services.context.Context;
import org.forgerock.util.Options;
import org.forgerock.util.promise.Promise;
@@ -358,12 +357,10 @@
.isMultiValued(mapper.get("isMultiValued").defaultTo(false).asBoolean())
.writability(parseWritability(mapper));
case "json":
- return simple(mapper.get("ldapAttribute").defaultTo(defaultLdapAttribute).required().asString())
+ return json(mapper.get("ldapAttribute").defaultTo(defaultLdapAttribute).required().asString())
.defaultJsonValue(mapper.get("defaultJsonValue").getObject())
.isRequired(mapper.get("isRequired").defaultTo(false).asBoolean())
.isMultiValued(mapper.get("isMultiValued").defaultTo(false).asBoolean())
- .encoder(JsonSchema.jsonToByteString())
- .decoder(JsonSchema.byteStringToJson())
.jsonSchema(mapper.isDefined("schema") ? mapper.get("schema") : null)
.writability(parseWritability(mapper));
case "reference":
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 225bf31..3c1e587 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
@@ -145,4 +145,6 @@
ERR_JSON_TRAILING_CONTENT_87=The value could not be parsed as valid JSON because it contains trailing content after the JSON value
ERR_JSON_EMPTY_CONTENT_88=The value could not be parsed as valid JSON because it is empty
ERR_JSON_QUERY_PARSE_ERROR_89=The value '%s' could not be parsed as a valid JSON query filter
-
+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'
diff --git a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/JsonPropertyMapperTest.java b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/JsonPropertyMapperTest.java
new file mode 100644
index 0000000..675f05b
--- /dev/null
+++ b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/JsonPropertyMapperTest.java
@@ -0,0 +1,61 @@
+/*
+ * 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.assertThat;
+import static org.forgerock.opendj.rest2ldap.FilterType.EQUAL_TO;
+import static org.forgerock.opendj.rest2ldap.FilterType.GREATER_THAN;
+
+import org.forgerock.json.JsonPointer;
+import org.forgerock.opendj.ldap.Filter;
+import org.forgerock.testng.ForgeRockTestCase;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+@SuppressWarnings("javadoc")
+@Test
+public class JsonPropertyMapperTest extends ForgeRockTestCase {
+ private final JsonPropertyMapper mapper = Rest2Ldap.json("json");
+
+ @DataProvider
+ static Object[][] ldapFilters() {
+ // @formatter:off
+ return new Object[][] {
+ { "x/y", "z", EQUAL_TO, "string", "(json=/z eq \"string\")" },
+ { "x/y", "", GREATER_THAN, 123, "(json=/ gt 123)" },
+ };
+ // @formatter:on
+ }
+
+ @Test(dataProvider = "ldapFilters")
+ public void testGetLdapFilter(final String path, final String subPath, final FilterType filterType,
+ final Object assertion, final String expected) throws Exception {
+ final Filter filter = mapper.getLdapFilter(null,
+ null,
+ new JsonPointer(path),
+ new JsonPointer(subPath),
+ filterType,
+ null,
+ assertion).getOrThrowUninterruptibly();
+ assertThat(filter.toString()).isEqualTo(expected);
+ }
+
+ @Test
+ public void testToJsonSchema() throws Exception {
+ assertThat(mapper.toJsonSchema()).isNotNull();
+ assertThat(mapper.toJsonSchema().isMap()).isTrue();
+ }
+}
--
Gitblit v1.10.0