From a2d67f674f7e80dcd9ca901ea63df41ef47f4214 Mon Sep 17 00:00:00 2001
From: vharseko <vharseko@openam.org.ru>
Date: Wed, 22 Nov 2017 05:08:57 +0000
Subject: [PATCH] Merge pull request #3 from GuyPaddock/wren/feature/subtree-flattening
---
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfigurator.java | 99 ++-
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceCollection.java | 106 +++
opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java | 725 ++++++++++++++++++++++---
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LdapHttpApplication.java | 7
opendj-rest2ldap/src/main/resources/org/forgerock/opendj/rest2ldap/rest2ldap.properties | 8
opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfiguratorTest.java | 394 +++++++++++++
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferencePropertyMapper.java | 42
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResource.java | 27
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Resource.java | 75 ++
opendj-server-legacy/resource/config/rest2ldap/endpoints/api/example-v1.json | 28 +
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceSingleton.java | 4
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReadOnlyRequestHandler.java | 16
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceImpl.java | 72 ++
opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/OAuth2JsonConfigurationTestCase.java | 5
opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/rest2ldap/endpoints/api/example-v1.json | 28 +
15 files changed, 1,431 insertions(+), 205 deletions(-)
diff --git a/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/rest2ldap/endpoints/api/example-v1.json b/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/rest2ldap/endpoints/api/example-v1.json
index 096de36..0bb2c83 100644
--- a/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/rest2ldap/endpoints/api/example-v1.json
+++ b/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/rest2ldap/endpoints/api/example-v1.json
@@ -21,6 +21,34 @@
"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
+ },
+ // This resource provides a read-only view of all users in the system, including
+ // users nested underneath entries like org units, organizations, etc., starting
+ // from "ou=people,dc=example,dc=com" and working down. It filters out any other
+ // structural elements, including organizations, org units, etc.
+ "all-users": {
+ "type": "collection",
+ "dnTemplate": "ou=people,dc=example,dc=com",
+ "resource": "frapi:opendj:rest2ldap:user:1.0",
+ "namingStrategy": {
+ "type": "clientDnNaming",
+ "dnAttribute": "uid"
+ },
+ "isReadOnly": true,
+ "flattenSubtree": true,
+ "baseSearchFilter": "(objectClass=person)"
+ },
"groups": {
"type": "collection",
"dnTemplate": "ou=groups,dc=example,dc=com",
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReadOnlyRequestHandler.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReadOnlyRequestHandler.java
index 1c16427..06fe696 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReadOnlyRequestHandler.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReadOnlyRequestHandler.java
@@ -12,12 +12,14 @@
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2016 ForgeRock AS.
- *
+ * Portions Copyright 2017 Rosie Applications, Inc.
*/
package org.forgerock.opendj.rest2ldap;
import static org.forgerock.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,15 @@
protected <V> Promise<V, ResourceException> handleRequest(final Context context, final Request request) {
return new BadRequestException(ERR_READ_ONLY_ENDPOINT.get().toString()).asPromise();
}
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public ApiDescription api(ApiProducer<ApiDescription> producer) {
+ if (delegate instanceof Describable) {
+ return ((Describable<ApiDescription, Request>)delegate).api(producer);
+ }
+ else {
+ return super.api(producer);
+ }
+ }
}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferencePropertyMapper.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferencePropertyMapper.java
index 9879681..5d9712f 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferencePropertyMapper.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferencePropertyMapper.java
@@ -12,6 +12,7 @@
* information: "Portions Copyright [year] [name of copyright owner]".
*
* Copyright 2012-2016 ForgeRock AS.
+ * Portions Copyright 2017 Rosie Applications, Inc.
*/
package org.forgerock.opendj.rest2ldap;
@@ -82,6 +83,7 @@
final String baseDnTemplate, final AttributeDescription primaryKey,
final PropertyMapper mapper) {
super(ldapAttributeName);
+
this.schema = schema;
this.baseDnTemplate = DnTemplate.compile(baseDnTemplate);
this.primaryKey = primaryKey;
@@ -89,13 +91,11 @@
}
/**
- * Sets the filter which should be used when searching for referenced LDAP
- * entries. The default is {@code (objectClass=*)}.
+ * Sets the filter which should be used when searching for referenced LDAP entries.
*
- * @param filter
- * The filter which should be used when searching for referenced
- * LDAP entries.
- * @return This property mapper.
+ * @param filter
+ * The filter which should be used when searching for referenced LDAP entries.
+ * @return This property mapper.
*/
public ReferencePropertyMapper searchFilter(final Filter filter) {
this.filter = checkNotNull(filter);
@@ -104,25 +104,24 @@
/**
* Sets the filter which should be used when searching for referenced LDAP
- * entries. The default is {@code (objectClass=*)}.
+ * entries.
*
- * @param filter
- * The filter which should be used when searching for referenced
- * LDAP entries.
- * @return This property mapper.
+ * @param filter
+ * The filter which should be used when searching for referenced LDAP entries.
+ * @return This property mapper.
*/
public ReferencePropertyMapper searchFilter(final String filter) {
return searchFilter(Filter.valueOf(filter));
}
/**
- * Sets the search scope which should be used when searching for referenced
- * LDAP entries. The default is {@link SearchScope#WHOLE_SUBTREE}.
+ * Sets the search scope which should be used when searching for referenced LDAP entries.
+ * The default is {@link SearchScope#WHOLE_SUBTREE}.
*
- * @param scope
- * The search scope which should be used when searching for
- * referenced LDAP entries.
- * @return This property mapper.
+ * @param scope
+ * The search scope which should be used when searching for
+ * referenced LDAP entries.
+ * @return This property mapper.
*/
public ReferencePropertyMapper searchScope(final SearchScope scope) {
this.scope = checkNotNull(scope);
@@ -142,9 +141,9 @@
return mapper.getLdapFilter(context, resource, path, subPath, type, operator, valueAssertion)
.thenAsync(new AsyncFunction<Filter, Filter, ResourceException>() {
@Override
- public Promise<Filter, ResourceException> apply(final Filter result) {
+ public Promise<Filter, ResourceException> apply(final Filter filter) {
// Search for all referenced entries and construct a filter.
- final SearchRequest request = createSearchRequest(context, result);
+ final SearchRequest request = createSearchRequest(context, filter);
final List<Filter> subFilters = new LinkedList<>();
return connectionFrom(context).searchAsync(request, new SearchResultHandler() {
@@ -325,8 +324,9 @@
}
}
- private SearchRequest createSearchRequest(final Context context, final Filter result) {
- final Filter searchFilter = filter != null ? Filter.and(filter, result) : result;
+ private SearchRequest createSearchRequest(final Context context, final Filter filter) {
+ final Filter searchFilter = this.filter != null ? Filter.and(this.filter, filter) : filter;
+
return newSearchRequest(baseDnTemplate.format(context), scope, searchFilter, "1.1");
}
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 9c8fbd0..4567647 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Resource.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Resource.java
@@ -12,6 +12,7 @@
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2016 ForgeRock AS.
+ * Portions Copyright 2017 Rosie Applications, Inc.
*/
package org.forgerock.opendj.rest2ldap;
@@ -34,6 +35,8 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
@@ -470,6 +473,46 @@
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) {
+ final StringBuilder serviceId = new StringBuilder(this.getResourceId());
+
+ if (isReadOnly) {
+ serviceId.append(":read-only");
+ } else {
+ serviceId.append(":read-write");
+ }
+
+ return serviceId.toString();
+ }
+
+ /**
+ * Gets a map of the sub-resources under this resource, keyed by URL template.
+ *
+ * @return The map of sub-resource URL templates to sub-resources.
+ */
+ Map<String, SubResource> getSubResourceMap() {
+ final Map<String, SubResource> result = new HashMap<>();
+
+ for (SubResource subResource : this.subResources) {
+ result.put(subResource.getUrlTemplate(), subResource);
+ }
+
+ return result;
+ }
+
void build(final Rest2Ldap rest2Ldap) {
// Prevent re-entrant calls.
if (isBuilt) {
@@ -522,7 +565,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 +582,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 +598,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));
- resource.create(createOperation(CreateMode.ID_FROM_SERVER));
+
+ if (!isReadOnly) {
+ resource.create(createOperation(CreateMode.ID_FROM_SERVER));
+ }
+
resource.query(Query.query()
.stability(EVOLVING)
.type(QueryType.FILTER)
@@ -580,23 +627,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();
}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LdapHttpApplication.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LdapHttpApplication.java
index 66e0a5c..5efd956 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LdapHttpApplication.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LdapHttpApplication.java
@@ -12,6 +12,7 @@
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2015-2016 ForgeRock AS.
+ * Portions Copyright 2017 Rosie Applications, Inc.
*/
package org.forgerock.opendj.rest2ldap;
@@ -371,7 +372,7 @@
rfc7662.get("clientId").required().asString(),
rfc7662.get("clientSecret").required().asString());
} catch (final URISyntaxException e) {
- throw new IllegalArgumentException(ERR_CONIFG_OAUTH2_INVALID_INTROSPECT_URL.get(
+ throw new IllegalArgumentException(ERR_CONFIG_OAUTH2_INVALID_INTROSPECT_URL.get(
introspectionEndPointURL, e.getLocalizedMessage()).toString(), e);
}
}
@@ -394,8 +395,8 @@
final Duration expiration = expirationJson.as(duration());
if (expiration.isZero() || expiration.isUnlimited()) {
throw newJsonValueException(expirationJson,
- expiration.isZero() ? ERR_CONIFG_OAUTH2_CACHE_ZERO_DURATION.get()
- : ERR_CONIFG_OAUTH2_CACHE_UNLIMITED_DURATION.get());
+ expiration.isZero() ? ERR_CONFIG_OAUTH2_CACHE_ZERO_DURATION.get()
+ : ERR_CONFIG_OAUTH2_CACHE_UNLIMITED_DURATION.get());
}
return expiration;
} catch (final Exception e) {
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfigurator.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfigurator.java
index 65572d5..1a794c7 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfigurator.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfigurator.java
@@ -12,7 +12,7 @@
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2016 ForgeRock AS.
- *
+ * Portions Copyright 2017 Rosie Applications, Inc.
*/
package org.forgerock.opendj.rest2ldap;
@@ -306,43 +306,84 @@
private enum NamingStrategyType { CLIENTDNNAMING, CLIENTNAMING, SERVERNAMING }
private enum SubResourceType { COLLECTION, SINGLETON }
- private static SubResource configureSubResource(final String urlTemplate, final JsonValue config) {
+ private static SubResource configureSubResource(final String urlTemplate,
+ final JsonValue config) {
final String dnTemplate = config.get("dnTemplate").defaultTo("").asString();
final Boolean isReadOnly = config.get("isReadOnly").defaultTo(false).asBoolean();
final String resourceId = config.get("resource").required().asString();
- if (config.get("type").required().as(enumConstant(SubResourceType.class)) == SubResourceType.COLLECTION) {
- final String[] glueObjectClasses = config.get("glueObjectClasses")
- .defaultTo(emptyList())
- .asList(String.class)
- .toArray(new String[0]);
+ final SubResourceType subResourceType =
+ config.get("type").required().as(enumConstant(SubResourceType.class));
- final SubResourceCollection collection = collectionOf(resourceId).urlTemplate(urlTemplate)
- .dnTemplate(dnTemplate)
- .isReadOnly(isReadOnly)
- .glueObjectClasses(glueObjectClasses);
-
- final JsonValue namingStrategy = config.get("namingStrategy").required();
- switch (namingStrategy.get("type").required().as(enumConstant(NamingStrategyType.class))) {
- case CLIENTDNNAMING:
- collection.useClientDnNaming(namingStrategy.get("dnAttribute").required().asString());
- break;
- case CLIENTNAMING:
- collection.useClientNaming(namingStrategy.get("dnAttribute").required().asString(),
- namingStrategy.get("idAttribute").required().asString());
- break;
- case SERVERNAMING:
- collection.useServerNaming(namingStrategy.get("dnAttribute").required().asString(),
- namingStrategy.get("idAttribute").required().asString());
- break;
- }
-
- return collection;
+ if (subResourceType == SubResourceType.COLLECTION) {
+ return configureCollectionSubResource(
+ config, resourceId, urlTemplate, dnTemplate, isReadOnly);
} else {
- return singletonOf(resourceId).urlTemplate(urlTemplate).dnTemplate(dnTemplate).isReadOnly(isReadOnly);
+ return configureSingletonSubResource(
+ config, resourceId, urlTemplate, dnTemplate, isReadOnly);
}
}
+ private static SubResource configureCollectionSubResource(final JsonValue config,
+ final String resourceId,
+ final String urlTemplate,
+ final String dnTemplate,
+ final Boolean isReadOnly) {
+ final String[] glueObjectClasses =
+ config.get("glueObjectClasses")
+ .defaultTo(emptyList())
+ .asList(String.class)
+ .toArray(new String[0]);
+
+ final Boolean flattenSubtree = config.get("flattenSubtree").defaultTo(false).asBoolean();
+ final String searchFilter = config.get("baseSearchFilter").asString();
+
+ final SubResourceCollection collection =
+ collectionOf(resourceId)
+ .urlTemplate(urlTemplate)
+ .dnTemplate(dnTemplate)
+ .isReadOnly(isReadOnly)
+ .glueObjectClasses(glueObjectClasses)
+ .flattenSubtree(flattenSubtree)
+ .baseSearchFilter(searchFilter);
+
+ configureCollectionNamingStrategy(config, collection);
+
+ return collection;
+ }
+
+ private static void configureCollectionNamingStrategy(final JsonValue config,
+ final SubResourceCollection collection) {
+ final JsonValue namingStrategy = config.get("namingStrategy").required();
+ final NamingStrategyType namingStrategyType =
+ namingStrategy.get("type").required().as(enumConstant(NamingStrategyType.class));
+
+ switch (namingStrategyType) {
+ case CLIENTDNNAMING:
+ collection.useClientDnNaming(namingStrategy.get("dnAttribute").required().asString());
+ break;
+ case CLIENTNAMING:
+ collection.useClientNaming(namingStrategy.get("dnAttribute").required().asString(),
+ namingStrategy.get("idAttribute").required().asString());
+ break;
+ case SERVERNAMING:
+ collection.useServerNaming(namingStrategy.get("dnAttribute").required().asString(),
+ namingStrategy.get("idAttribute").required().asString());
+ break;
+ }
+ }
+
+ private static SubResource configureSingletonSubResource(final JsonValue config,
+ final String resourceId,
+ final String urlTemplate,
+ final String dnTemplate,
+ final Boolean isReadOnly) {
+ return singletonOf(resourceId)
+ .urlTemplate(urlTemplate)
+ .dnTemplate(dnTemplate)
+ .isReadOnly(isReadOnly);
+ }
+
private static PropertyMapper configurePropertyMapper(final JsonValue mapper, final String defaultLdapAttribute) {
switch (mapper.get("type").required().asString()) {
case "resourceType":
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResource.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResource.java
index 0e61043..4c96470 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResource.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResource.java
@@ -12,7 +12,7 @@
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2016 ForgeRock AS.
- *
+ * Portions Copyright 2017 Rosie Applications, Inc.
*/
package org.forgerock.opendj.rest2ldap;
@@ -60,9 +60,10 @@
String urlTemplate = "";
String dnTemplateString = "";
- boolean isReadOnly = false;
- Rest2Ldap rest2Ldap;
- Resource resource;
+
+ protected boolean isReadOnly = false;
+ protected Rest2Ldap rest2Ldap;
+ protected Resource resource;
SubResource(final String resourceId) {
this.resourceId = resourceId;
@@ -80,9 +81,27 @@
@Override
public final String toString() {
+ return getUrlTemplate();
+ }
+
+ /**
+ * Gets the URL template that must match for this sub-resource to apply to a given request.
+ *
+ * @return The URL template for this sub-resource.
+ */
+ public String getUrlTemplate() {
return urlTemplate;
}
+ /**
+ * Gets whether or not this sub-resource has been configured for read-only access.
+ *
+ * @return {@code true} if the sub-resource is read-only; {@code false} otherwise.
+ */
+ public boolean isReadOnly() {
+ return isReadOnly;
+ }
+
final Resource getResource() {
return resource;
}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceCollection.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceCollection.java
index 7713ced..0e27feb 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceCollection.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceCollection.java
@@ -12,6 +12,7 @@
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2016 ForgeRock AS.
+ * Portions Copyright 2017 Rosie Applications, Inc.
*/
package org.forgerock.opendj.rest2ldap;
@@ -74,13 +75,39 @@
private final Attribute glueObjectClasses = new LinkedAttribute("objectClass");
private NamingStrategy namingStrategy;
+ private boolean flattenSubtree;
+ private Filter baseSearchFilter;
SubResourceCollection(final String resourceId) {
super(resourceId);
+
useClientDnNaming("uid");
}
/**
+ * Gets whether or not this sub-resource should flatten sub-entries in results.
+ *
+ * @return {@code true} if entries deep in the sub-tree are included in a flattened
+ * collection view; {@code false} if only entries at the top level of the DN for this
+ * sub-resource should be returned.
+ */
+ public boolean shouldFlattenSubtree() {
+ return flattenSubtree;
+ }
+
+ /**
+ * Gets the base filter that always restricts what LDAP entries are accessible through this
+ * collection, before any filters are applied from the request itself.
+ *
+ * The default is {@code null} (no base filter restriction at all).
+ *
+ * @return Either a search filter; or {@code null} if no base search filter has been defined.
+ */
+ public Filter getBaseSearchFilter() {
+ return baseSearchFilter;
+ }
+
+ /**
* Indicates that the JSON resource ID must be provided by the user, and will be used for naming the associated LDAP
* entry. More specifically, LDAP entry names will be derived by appending a single RDN to the collection's base DN
* composed of the specified attribute type and LDAP value taken from the LDAP entry once attribute mapping has been
@@ -213,12 +240,72 @@
/**
* Indicates whether this sub-resource collection only supports read and query operations.
*
- * @param readOnly
+ * @param isReadOnly
* {@code true} if this sub-resource collection is read-only.
* @return A reference to this object.
*/
- public SubResourceCollection isReadOnly(final boolean readOnly) {
- isReadOnly = readOnly;
+ public SubResourceCollection isReadOnly(final boolean isReadOnly) {
+ this.isReadOnly = isReadOnly;
+ return this;
+ }
+
+ /**
+ * Controls whether or not LDAP entries in the hierarchy below the root entry of the resource
+ * collection are included in the list of resources (essentially, flattening the hierarchy
+ * into one collection of resources).
+ *
+ * This can only be used if the resource is read-only. The default is not to flatten, which
+ * preserves the legacy behavior of Rest2LDAP.
+ *
+ * @param flattenSubtree
+ * Whether or not to flatten the hierarchy by searching the entire subtree.
+ * @return A reference to this object.
+ * @throws IllegalArgumentException
+ * If the configuration is invalid.
+ */
+ public SubResourceCollection flattenSubtree(boolean flattenSubtree) {
+ if (flattenSubtree && !this.isReadOnly) {
+ throw new LocalizedIllegalArgumentException(
+ ERR_CONFIG_MUST_BE_READ_ONLY_TO_FLATTEN_SUBTREE.get());
+ }
+
+ this.flattenSubtree = flattenSubtree;
+ return this;
+ }
+
+ /**
+ * Sets the base filter that always restricts what LDAP entries are accessible through this
+ * collection, before any filters are applied from the request itself.
+ *
+ * The default is {@code null} (no base filter restriction at all).
+ *
+ * @param filter
+ * The filter which should be used to restrict which LDAP entries are returned.
+ * @return A reference to this object.
+ */
+ public SubResourceCollection baseSearchFilter(final Filter filter) {
+ this.baseSearchFilter = filter;
+ return this;
+ }
+
+ /**
+ * Sets the base filter that always restricts what LDAP entries are accessible through this
+ * collection, before any filters are applied from the request itself.
+ *
+ * The default is {@code null} (no base filter restriction at all).
+ *
+ * @param filter
+ * The filter which should be used to restrict which LDAP entries are returned.
+ * @return A reference to this object.
+ */
+ public SubResourceCollection baseSearchFilter(final String filter) {
+ if (filter == null) {
+ baseSearchFilter((Filter)null);
+ }
+ else {
+ baseSearchFilter(Filter.valueOf(filter));
+ }
+
return this;
}
@@ -256,11 +343,14 @@
}
private SubResourceImpl collection(final Context context) {
- return new SubResourceImpl(rest2Ldap,
- dnFrom(context),
- dnTemplateString.isEmpty() ? null : glueObjectClasses,
- namingStrategy,
- resource);
+ return new SubResourceImpl(
+ rest2Ldap,
+ dnFrom(context),
+ dnTemplateString.isEmpty() ? null : glueObjectClasses,
+ namingStrategy,
+ resource,
+ flattenSubtree,
+ baseSearchFilter);
}
private String idFrom(final Context context) {
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceImpl.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceImpl.java
index 7cec13f..f731a1b 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceImpl.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceImpl.java
@@ -12,6 +12,7 @@
* information: "Portions Copyright [year] [name of copyright owner]".
*
* Copyright 2012-2016 ForgeRock AS.
+ * Portions Copyright 2017 Rosie Applications, Inc.
*/
package org.forgerock.opendj.rest2ldap;
@@ -31,7 +32,6 @@
import static org.forgerock.opendj.ldap.ByteString.valueOfBytes;
import static org.forgerock.opendj.ldap.Filter.alwaysFalse;
import static org.forgerock.opendj.ldap.Filter.alwaysTrue;
-import static org.forgerock.opendj.ldap.SearchScope.SINGLE_LEVEL;
import static org.forgerock.opendj.ldap.requests.Requests.*;
import static org.forgerock.opendj.rest2ldap.ReadOnUpdatePolicy.CONTROLS;
import static org.forgerock.opendj.rest2ldap.RoutingContext.newCollectionRoutingContext;
@@ -140,9 +140,17 @@
private final boolean usePermissiveModify;
private final Resource resource;
private final Attribute glueObjectClasses;
+ private final boolean flattenSubtree;
+ private final Filter baseSearchFilter;
SubResourceImpl(final Rest2Ldap rest2Ldap, final DN baseDn, final Attribute glueObjectClasses,
final NamingStrategy namingStrategy, final Resource resource) {
+ this(rest2Ldap, baseDn, glueObjectClasses, namingStrategy, resource, false, null);
+ }
+
+ SubResourceImpl(final Rest2Ldap rest2Ldap, final DN baseDn, final Attribute glueObjectClasses,
+ final NamingStrategy namingStrategy, final Resource resource,
+ final boolean flattenSubtree, final Filter baseSearchFilter) {
this.readOnUpdatePolicy = rest2Ldap.getOptions().get(READ_ON_UPDATE_POLICY);
this.useSubtreeDelete = rest2Ldap.getOptions().get(USE_SUBTREE_DELETE);
this.usePermissiveModify = rest2Ldap.getOptions().get(USE_PERMISSIVE_MODIFY);
@@ -153,6 +161,8 @@
this.glueObjectClasses = glueObjectClasses;
this.namingStrategy = namingStrategy;
this.resource = resource;
+ this.flattenSubtree = flattenSubtree;
+ this.baseSearchFilter = baseSearchFilter;
}
Promise<ActionResponse, ResourceException> action(
@@ -504,9 +514,34 @@
Promise<QueryResponse, ResourceException> query(
final Context context, final QueryRequest request, final QueryResourceHandler resourceHandler) {
return getLdapFilter(context, request.getQueryFilter())
+ .then(applyBaseSearchFilter())
.thenAsync(runQuery(context, request, resourceHandler));
}
+ /**
+ * Generates a function that applies any base filter that this sub-resource may have been
+ * initialized with.
+ *
+ * @return The function to invoke to apply a base filter, if one has been specified.
+ */
+ private Function<Filter, Filter, ResourceException> applyBaseSearchFilter() {
+ return new Function<Filter, Filter, ResourceException>() {
+ @Override
+ public Filter apply(final Filter requestFilter) throws ResourceException {
+ final Filter baseSearchFilter = SubResourceImpl.this.baseSearchFilter,
+ searchFilter;
+
+ if (baseSearchFilter != null) {
+ searchFilter = Filter.and(baseSearchFilter, requestFilter);
+ } else {
+ searchFilter = requestFilter;
+ }
+
+ return searchFilter;
+ }
+ };
+ }
+
// FIXME: supporting assertions against sub-type properties.
private Promise<Filter, ResourceException> getLdapFilter(
final Context context, final QueryFilter<JsonPointer> queryFilter) {
@@ -523,6 +558,7 @@
public Promise<Filter, ResourceException> visitAndFilter(
final Void unused, final List<QueryFilter<JsonPointer>> subFilters) {
final List<Promise<Filter, ResourceException>> promises = new ArrayList<>(subFilters.size());
+
for (final QueryFilter<JsonPointer> subFilter : subFilters) {
promises.add(subFilter.accept(this, unused));
}
@@ -532,14 +568,17 @@
public Filter apply(final List<Filter> value) {
// Check for unmapped filter components and optimize.
final Iterator<Filter> i = value.iterator();
+
while (i.hasNext()) {
final Filter f = i.next();
+
if (f == alwaysFalse()) {
return alwaysFalse();
} else if (f == alwaysTrue()) {
i.remove();
}
}
+
switch (value.size()) {
case 0:
return alwaysTrue();
@@ -673,6 +712,7 @@
parentDnAndType, resource, ROOT, field, STARTS_WITH, null, valueAssertion);
}
};
+
// Note that the returned LDAP filter may be null if it could not be mapped by any property mappers.
return queryFilter.accept(visitor, null);
}
@@ -700,7 +740,7 @@
final String[] attributes = getLdapAttributesForUnknownType(request.getFields()).toArray(new String[0]);
final Filter searchFilter = ldapFilter == Filter.alwaysTrue() ? Filter.objectClassPresent()
: ldapFilter;
- final SearchRequest searchRequest = newSearchRequest(baseDn, SINGLE_LEVEL, searchFilter, attributes);
+ final SearchRequest searchRequest = createSearchRequest(searchFilter, attributes);
// Add the page results control. We can support the page offset by reading the next offset pages, or
// offset x page size resources.
@@ -1064,6 +1104,34 @@
return namingStrategy.createSearchRequest(baseDn, resourceId).addAttribute(attributes);
}
+ /**
+ * Creates a request to search LDAP for entries that match the provided search filter, and
+ * the specified attributes.
+ *
+ * If the subtree flattening is enabled, the search request will encompass the whole subtree.
+ *
+ * @param searchFilter
+ * The filter that entries must match to be returned.
+ * @param desiredAttributes
+ * The names of the attributes to be included with each entry.
+ *
+ * @return The resulting search request.
+ */
+ private SearchRequest createSearchRequest(Filter searchFilter, String[] desiredAttributes) {
+ final SearchScope searchScope;
+ final SearchRequest searchRequest;
+
+ if (SubResourceImpl.this.flattenSubtree) {
+ searchScope = SearchScope.SUBORDINATES;
+ } else {
+ searchScope = SearchScope.SINGLE_LEVEL;
+ }
+
+ searchRequest = newSearchRequest(baseDn, searchScope, searchFilter, desiredAttributes);
+
+ return searchRequest;
+ }
+
@SuppressWarnings("unused")
private static <R> AsyncFunction<LdapException, R, ResourceException> adaptLdapException(final Class<R> clazz) {
return new AsyncFunction<LdapException, R, ResourceException>() {
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceSingleton.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceSingleton.java
index 77976f2..6182a24 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceSingleton.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceSingleton.java
@@ -12,6 +12,7 @@
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2016 ForgeRock AS.
+ * Portions Copyright 2017 Rosie Applications, Inc.
*/
package org.forgerock.opendj.rest2ldap;
@@ -139,7 +140,8 @@
}
private SubResourceImpl singleton(final Context context) {
- return new SubResourceImpl(rest2Ldap, dnFrom(context), null, SINGLETON_NAMING_STRATEGY, resource);
+ return new SubResourceImpl(
+ rest2Ldap, dnFrom(context), null, SINGLETON_NAMING_STRATEGY, resource);
}
/**
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 3c1e587..40631a1 100644
--- a/opendj-rest2ldap/src/main/resources/org/forgerock/opendj/rest2ldap/rest2ldap.properties
+++ b/opendj-rest2ldap/src/main/resources/org/forgerock/opendj/rest2ldap/rest2ldap.properties
@@ -12,14 +12,15 @@
# information: "Portions Copyright [year] [name of copyright owner]".
#
# Copyright 2016 ForgeRock AS.
+# Portions Copyright 2017 Rosie Applications, Inc.
#
# Configuration errors
ERR_FAIL_PARSE_CONFIGURATION_1=Unable to start Rest2Ldap Http Application due to the configuration error: '%s'
ERR_CONFIG_OAUTH2_UNSUPPORTED_ACCESS_TOKEN_RESOLVER_2= '%s'is not a supported access token resolver. Must be one of '%s'
-ERR_CONIFG_OAUTH2_INVALID_INTROSPECT_URL_3=The token introspection endpoint '%s' is not a valid URL: '%s'
-ERR_CONIFG_OAUTH2_CACHE_ZERO_DURATION_4=The cache expiration duration cannot be zero
-ERR_CONIFG_OAUTH2_CACHE_UNLIMITED_DURATION_5=The cache expiration duration cannot be unlimited
+ERR_CONFIG_OAUTH2_INVALID_INTROSPECT_URL_3=The token introspection endpoint '%s' is not a valid URL: '%s'
+ERR_CONFIG_OAUTH2_CACHE_ZERO_DURATION_4=The cache expiration duration cannot be zero
+ERR_CONFIG_OAUTH2_CACHE_UNLIMITED_DURATION_5=The cache expiration duration cannot be unlimited
ERR_CONFIG_OAUTH2_CACHE_INVALID_DURATION_6=Malformed duration value '%s' for cache expiration. \
The duration syntax supports all human readable notations from day ('days'', 'day'', 'd'') to nanosecond \
('nanoseconds', 'nanosecond', 'nanosec', 'nanos', 'nano', 'ns')
@@ -148,3 +149,4 @@
ERR_PATCH_JSON_INTERNAL_PROPERTY_90=The patch request cannot be processed because it attempts to modify the \
internal field '%s' of object '%s'. This capability is not currently supported by Rest2Ldap. Applications should \
instead perform a patch which replaces the entire object '%s'
+ERR_CONFIG_MUST_BE_READ_ONLY_TO_FLATTEN_SUBTREE_91=Sub-resources must be read-only to support sub-tree flattening.
diff --git a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java
index 343ed60..cb1bab0 100644
--- a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java
+++ b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java
@@ -12,6 +12,7 @@
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2013-2016 ForgeRock AS.
+ * Portions Copyright 2017 Rosie Applications, Inc.
*/
package org.forgerock.opendj.rest2ldap;
@@ -92,72 +93,331 @@
private static final QueryFilter<JsonPointer> NO_FILTER = QueryFilter.alwaysTrue();
@Test
- public void testQueryAll() throws Exception {
+ public void testQueryAllWithNoSubtreeFlatteningAndNoSearchFilter() throws Exception {
final Connection connection = newConnection();
final List<ResourceResponse> resources = new LinkedList<>();
- final QueryResponse result = connection.query(
- newAuthConnectionContext(), newQueryRequest("").setQueryFilter(NO_FILTER), resources);
- assertThat(resources).hasSize(5);
+ final QueryResponse result =
+ connection.query(
+ newAuthConnectionContext(),
+ newQueryRequest("").setQueryFilter(NO_FILTER),
+ resources);
+
+ assertThat(resources).hasSize(7);
assertThat(result.getPagedResultsCookie()).isNull();
assertThat(result.getTotalPagedResults()).isEqualTo(-1);
+
+ checkThatOrgUnitsExist(resources, "level1");
+
+ checkThatUsersExist(resources, 1,
+ "test1",
+ "test2",
+ "test3",
+ "test4",
+ "test5",
+ "test6"
+ );
}
@Test
- public void testQueryNone() throws Exception {
+ public void testQueryAllWithSearchFilterAndNoSubtreeFlattening() throws Exception {
final Connection connection = newConnection();
final List<ResourceResponse> resources = new LinkedList<>();
- final QueryResponse result = connection.query(newAuthConnectionContext(),
- newQueryRequest("").setQueryFilter(QueryFilter.<JsonPointer> alwaysFalse()), resources);
+ final QueryResponse result =
+ connection.query(
+ newAuthConnectionContext(),
+ newQueryRequest("top-level-users").setQueryFilter(NO_FILTER),
+ resources);
+
+ assertThat(resources).hasSize(6);
+ assertThat(result.getPagedResultsCookie()).isNull();
+ assertThat(result.getTotalPagedResults()).isEqualTo(-1);
+
+ checkThatUsersExist(resources,
+ "test1",
+ "test2",
+ "test3",
+ "test4",
+ "test5",
+ "test6"
+ );
+ }
+
+ @Test
+ public void testQueryAllWithSubtreeFlatteningAndNoSearchFilter() throws Exception {
+ final Connection connection = newConnection();
+ final List<ResourceResponse> resources = new LinkedList<>();
+ final QueryResponse result =
+ connection.query(
+ newAuthConnectionContext(),
+ newQueryRequest("all-entries").setQueryFilter(NO_FILTER),
+ resources);
+
+ assertThat(resources).hasSize(10);
+ assertThat(result.getPagedResultsCookie()).isNull();
+ assertThat(result.getTotalPagedResults()).isEqualTo(-1);
+
+ checkThatOrgUnitsExist(resources,
+ "level1",
+ "level2"
+ );
+
+ checkThatUsersExist(resources, 2,
+ "sub2",
+ "sub1",
+ "test1",
+ "test2",
+ "test3",
+ "test4",
+ "test5",
+ "test6"
+ );
+ }
+
+ @Test
+ public void testQueryAllWithSubtreeFlatteningAndSearchFilter() throws Exception {
+ final Connection connection = newConnection();
+ final List<ResourceResponse> resources = new LinkedList<>();
+ final QueryResponse result =
+ connection.query(
+ newAuthConnectionContext(),
+ newQueryRequest("all-users").setQueryFilter(NO_FILTER),
+ resources);
+
+ assertThat(resources).hasSize(8);
+ assertThat(result.getPagedResultsCookie()).isNull();
+ assertThat(result.getTotalPagedResults()).isEqualTo(-1);
+
+ checkThatUsersExist(resources,
+ "sub2",
+ "sub1",
+ "test1",
+ "test2",
+ "test3",
+ "test4",
+ "test5",
+ "test6"
+ );
+ }
+
+ @Test
+ public void testQueryNoneWithNoSubtreeFlatteningAndNoSearchFilter() throws Exception {
+ final Connection connection = newConnection();
+ final List<ResourceResponse> resources = new LinkedList<>();
+ final QueryResponse result =
+ connection.query(
+ newAuthConnectionContext(),
+ newQueryRequest("").setQueryFilter(QueryFilter.<JsonPointer> alwaysFalse()),
+ resources);
+
assertThat(resources).hasSize(0);
assertThat(result.getPagedResultsCookie()).isNull();
assertThat(result.getTotalPagedResults()).isEqualTo(-1);
}
@Test
- public void testQueryPageResultsCookie() throws Exception {
+ public void testQueryNoneWithSearchFilterAndNoSubtreeFlattening() throws Exception {
+ final Connection connection = newConnection();
+ final List<ResourceResponse> resources = new LinkedList<>();
+ final QueryResponse result =
+ connection.query(
+ newAuthConnectionContext(),
+ newQueryRequest("top-level-users")
+ .setQueryFilter(QueryFilter.<JsonPointer> alwaysFalse()),
+ resources);
+
+ assertThat(resources).hasSize(0);
+ assertThat(result.getPagedResultsCookie()).isNull();
+ assertThat(result.getTotalPagedResults()).isEqualTo(-1);
+ }
+
+ @Test
+ public void testQueryNoneWithSubtreeFlatteningAndNoSearchFilter() throws Exception {
+ final Connection connection = newConnection();
+ final List<ResourceResponse> resources = new LinkedList<>();
+ final QueryResponse result =
+ connection.query(
+ newAuthConnectionContext(),
+ newQueryRequest("all-entries")
+ .setQueryFilter(QueryFilter.<JsonPointer> alwaysFalse()),
+ resources);
+
+ assertThat(resources).hasSize(0);
+ assertThat(result.getPagedResultsCookie()).isNull();
+ assertThat(result.getTotalPagedResults()).isEqualTo(-1);
+ }
+
+ @Test
+ public void testQueryNoneWithSubtreeFlatteningAndSearchFilter() throws Exception {
+ final Connection connection = newConnection();
+ final List<ResourceResponse> resources = new LinkedList<>();
+ final QueryResponse result = connection.query(newAuthConnectionContext(),
+ newQueryRequest("all-users").setQueryFilter(QueryFilter.<JsonPointer> alwaysFalse()), resources);
+
+ assertThat(resources).hasSize(0);
+ assertThat(result.getPagedResultsCookie()).isNull();
+ assertThat(result.getTotalPagedResults()).isEqualTo(-1);
+ }
+
+ @Test
+ public void testQueryPageResultsCookieWithNoSubtreeFlatteningAndNoSearchFilter()
+ throws Exception {
final Connection connection = newConnection();
final List<ResourceResponse> resources = new ArrayList<>();
// Read first page.
- QueryResponse result = connection.query(
- newAuthConnectionContext(), newQueryRequest("").setQueryFilter(NO_FILTER).setPageSize(2), resources);
+ QueryResponse result =
+ connection.query(
+ newAuthConnectionContext(),
+ newQueryRequest("")
+ .setQueryFilter(NO_FILTER)
+ .setPageSize(3),
+ resources);
+
assertThat(result.getPagedResultsCookie()).isNotNull();
- assertThat(resources).hasSize(2);
- assertThat(resources.get(0).getId()).isEqualTo("test1");
- assertThat(resources.get(1).getId()).isEqualTo("test2");
+ assertThat(resources).hasSize(3);
+
+ checkThatOrgUnitsExist(resources, "level1");
+
+ checkThatUsersExist(resources, 1,
+ "test1",
+ "test2");
String cookie = result.getPagedResultsCookie();
+
resources.clear();
// Read second page.
- result = connection.query(newAuthConnectionContext(),
- newQueryRequest("").setQueryFilter(NO_FILTER).setPageSize(2).setPagedResultsCookie(cookie), resources);
+ result =
+ connection.query(
+ newAuthConnectionContext(),
+ newQueryRequest("")
+ .setQueryFilter(NO_FILTER)
+ .setPageSize(3)
+ .setPagedResultsCookie(cookie),
+ resources);
+
assertThat(result.getPagedResultsCookie()).isNotNull();
- assertThat(resources).hasSize(2);
- assertThat(resources.get(0).getId()).isEqualTo("test3");
- assertThat(resources.get(1).getId()).isEqualTo("test4");
+ assertThat(resources).hasSize(3);
+
+ checkThatUsersExist(resources,
+ "test3",
+ "test4",
+ "test5");
cookie = result.getPagedResultsCookie();
+
resources.clear();
// Read third page.
- result = connection.query(newAuthConnectionContext(),
- newQueryRequest("").setQueryFilter(NO_FILTER).setPageSize(2).setPagedResultsCookie(cookie), resources);
+ result =
+ connection.query(
+ newAuthConnectionContext(),
+ newQueryRequest("")
+ .setQueryFilter(NO_FILTER)
+ .setPageSize(3)
+ .setPagedResultsCookie(cookie),
+ resources);
+
assertThat(result.getPagedResultsCookie()).isNull();
assertThat(resources).hasSize(1);
- assertThat(resources.get(0).getId()).isEqualTo("test5");
+
+ checkThatUsersExist(resources,
+ "test6");
}
@Test
- public void testQueryPageResultsIndexed() throws Exception {
+ public void testQueryPageResultsCookieWithSubtreeFlatteningAndSearchFilter() throws Exception {
final Connection connection = newConnection();
final List<ResourceResponse> resources = new ArrayList<>();
- QueryResponse result = connection.query(newAuthConnectionContext(),
- newQueryRequest("").setQueryFilter(NO_FILTER).setPageSize(2).setPagedResultsOffset(1), resources);
+
+ // Read first page.
+ QueryResponse result =
+ connection.query(
+ newAuthConnectionContext(),
+ newQueryRequest("all-users")
+ .setQueryFilter(NO_FILTER)
+ .setPageSize(5),
+ resources);
+
+ assertThat(result.getPagedResultsCookie()).isNotNull();
+ assertThat(resources).hasSize(5);
+
+ checkThatUsersExist(resources,
+ "sub2",
+ "sub1",
+ "test1",
+ "test2",
+ "test3");
+
+ String cookie = result.getPagedResultsCookie();
+
+ resources.clear();
+
+ // Read second page.
+ result =
+ connection.query(
+ newAuthConnectionContext(),
+ newQueryRequest("all-users")
+ .setQueryFilter(NO_FILTER)
+ .setPageSize(5)
+ .setPagedResultsCookie(cookie),
+ resources);
+
+ assertThat(result.getPagedResultsCookie()).isNull();
+ assertThat(resources).hasSize(3);
+
+ checkThatUsersExist(resources,
+ "test4",
+ "test5",
+ "test6");
+
+ resources.clear();
+ }
+
+ @Test
+ public void testQueryPageResultsIndexedWithNoSubtreeFlatteningAndNoSearchFilter()
+ throws Exception {
+ final Connection connection = newConnection();
+ final List<ResourceResponse> resources = new ArrayList<>();
+
+ QueryResponse result =
+ connection.query(
+ newAuthConnectionContext(),
+ newQueryRequest("")
+ .setQueryFilter(NO_FILTER)
+ .setPageSize(2)
+ .setPagedResultsOffset(1),
+ resources);
+
assertThat(result.getPagedResultsCookie()).isNotNull();
assertThat(resources).hasSize(2);
- assertThat(resources.get(0).getId()).isEqualTo("test3");
- assertThat(resources.get(1).getId()).isEqualTo("test4");
+
+ checkThatUsersExist(resources,
+ "test2",
+ "test3");
+ }
+
+ @Test
+ public void testQueryPageResultsIndexedWithSubtreeFlatteningAndSearchFilter() throws Exception {
+ final Connection connection = newConnection();
+ final List<ResourceResponse> resources = new ArrayList<>();
+
+ QueryResponse result =
+ connection.query(
+ newAuthConnectionContext(),
+ newQueryRequest("all-users")
+ .setQueryFilter(NO_FILTER)
+ .setPageSize(3)
+ .setPagedResultsOffset(1),
+ resources);
+
+ assertThat(result.getPagedResultsCookie()).isNotNull();
+ assertThat(resources).hasSize(3);
+
+ checkThatUsersExist(resources,
+ "test2",
+ "test3",
+ "test4");
}
@Test(expectedExceptions = NotFoundException.class)
@@ -165,6 +425,7 @@
final Context context = newAuthConnectionContext();
final Connection connection = newConnection();
final ResourceResponse resource = connection.delete(context, newDeleteRequest("/test1"));
+
checkResourcesAreEqual(resource, getTestUser1(12345));
connection.read(context, newReadRequest("/test1"));
}
@@ -173,7 +434,9 @@
public void testDeleteMVCCMatch() throws Exception {
final Context context = newAuthConnectionContext();
final Connection connection = newConnection();
- final ResourceResponse resource = connection.delete(context, newDeleteRequest("/test1").setRevision("12345"));
+ final ResourceResponse resource =
+ connection.delete(context, newDeleteRequest("/test1").setRevision("12345"));
+
checkResourcesAreEqual(resource, getTestUser1(12345));
connection.read(context, newReadRequest("/test1"));
}
@@ -182,6 +445,7 @@
public void testDeleteMVCCNoMatch() throws Exception {
final Context context = newAuthConnectionContext();
final Connection connection = newConnection();
+
connection.delete(context, newDeleteRequest("/test1").setRevision("12346"));
}
@@ -189,6 +453,7 @@
public void testDeleteNotFound() throws Exception {
final Context context = newAuthConnectionContext();
final Connection connection = newConnection();
+
connection.delete(context, newDeleteRequest("/missing"));
}
@@ -199,6 +464,7 @@
final ResourceResponse resource1 = connection.patch(context,
newPatchRequest("/test1", add("/name/displayName", "changed")));
checkResourcesAreEqual(resource1, getTestUser1Updated(12345));
+
final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
checkResourcesAreEqual(resource2, getTestUser1Updated(12345));
}
@@ -209,6 +475,7 @@
final Context context = newAuthConnectionContext(requests);
final Connection connection = newConnection();
final ResourceResponse resource1 = connection.patch(context, newPatchRequest("/test1"));
+
checkResourcesAreEqual(resource1, getTestUser1(12345));
/*
@@ -227,11 +494,16 @@
final Context context = newAuthConnectionContext();
final Connection connection = newConnection();
final JsonValue newContent = getTestUser1(12345);
+
newContent.put("description", asList("one", "two"));
+
final ResourceResponse resource1 =
connection.patch(context,
- newPatchRequest("/test1", add("/description", asList("one", "two"))));
+ newPatchRequest(
+ "/test1",
+ add("/description", asList("one", "two"))));
checkResourcesAreEqual(resource1, newContent);
+
final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
checkResourcesAreEqual(resource2, newContent);
}
@@ -241,26 +513,37 @@
final Context context = newAuthConnectionContext();
final Connection connection = newConnection();
final JsonValue newContent = getTestUser1(12345);
+
newContent.put("description", asList("one", "two"));
- final ResourceResponse resource1 = connection.patch(
- context, newPatchRequest("/test1", add("/description/-", "one"), add("/description/-", "two")));
+
+ final ResourceResponse resource1 =
+ connection.patch(
+ context,
+ newPatchRequest("/test1", add("/description/-", "one"),
+ add("/description/-", "two")));
checkResourcesAreEqual(resource1, newContent);
+
final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
checkResourcesAreEqual(resource2, newContent);
}
@Test(expectedExceptions = BadRequestException.class)
public void testPatchConstantAttribute() throws Exception {
- newConnection().patch(newAuthConnectionContext(), newPatchRequest("/test1", add("/schemas", asList("junk"))));
+ newConnection().patch(
+ newAuthConnectionContext(),
+ newPatchRequest("/test1", add("/schemas", asList("junk"))));
}
@Test
public void testPatchDeleteOptionalAttribute() throws Exception {
final Context context = newAuthConnectionContext();
final Connection connection = newConnection();
+
connection.patch(context, newPatchRequest("/test1", add("/description", asList("one", "two"))));
+
final ResourceResponse resource1 = connection.patch(context, newPatchRequest("/test1", remove("/description")));
checkResourcesAreEqual(resource1, getTestUser1(12345));
+
final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
checkResourcesAreEqual(resource2, getTestUser1(12345));
}
@@ -270,34 +553,52 @@
final Context context = newAuthConnectionContext();
final Connection connection = newConnection();
final JsonValue newContent = getTestUser1(12345);
+
newContent.put("singleNumber", 100);
newContent.put("multiNumber", asList(200, 300));
- final ResourceResponse resource1 = connection.patch(context, newPatchRequest("/test1",
- add("/singleNumber", 0),
- add("/multiNumber", asList(100, 200)),
- increment("/singleNumber", 100),
- increment("/multiNumber", 100)));
+ final ResourceResponse resource1 =
+ connection.patch(
+ context,
+ newPatchRequest(
+ "/test1",
+ add("/singleNumber", 0),
+ add("/multiNumber", asList(100, 200)),
+ increment("/singleNumber", 100),
+ increment("/multiNumber", 100)));
+
checkResourcesAreEqual(resource1, newContent);
+
final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
checkResourcesAreEqual(resource2, newContent);
}
@Test(expectedExceptions = BadRequestException.class)
public void testPatchMissingRequiredAttribute() throws Exception {
- newConnection().patch(newAuthConnectionContext(), newPatchRequest("/test1", remove("/name/surname")));
+ newConnection().patch(
+ newAuthConnectionContext(),
+ newPatchRequest("/test1", remove("/name/surname")));
}
@Test
public void testPatchModifyOptionalAttribute() throws Exception {
final Connection connection = newConnection();
final Context context = newAuthConnectionContext();
- connection.patch(context, newPatchRequest("/test1", add("/description", asList("one", "two"))));
+
+ connection.patch(
+ context,
+ newPatchRequest("/test1", add("/description", asList("one", "two"))));
+
final ResourceResponse resource1 =
- connection.patch(context, newPatchRequest("/test1", add("/description", asList("three"))));
+ connection.patch(
+ context,
+ newPatchRequest("/test1", add("/description", asList("three"))));
+
final JsonValue newContent = getTestUser1(12345);
+
newContent.put("description", asList("one", "two", "three"));
checkResourcesAreEqual(resource1, newContent);
+
final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
checkResourcesAreEqual(resource2, newContent);
}
@@ -305,29 +606,42 @@
@Test(expectedExceptions = NotSupportedException.class)
public void testPatchMultiValuedAttributeIndexAppend() throws Exception {
final Connection connection = newConnection();
- connection.patch(newAuthConnectionContext(), newPatchRequest("/test1", add("/description/0", "junk")));
+
+ connection.patch(
+ newAuthConnectionContext(),
+ newPatchRequest("/test1", add("/description/0", "junk")));
}
@Test(expectedExceptions = BadRequestException.class)
public void testPatchMultiValuedAttributeIndexAppendWithList() throws Exception {
final Connection connection = newConnection();
- connection.patch(newAuthConnectionContext(), newPatchRequest("/test1", add("/description/-",
- asList("one", "two"))));
+
+ connection.patch(
+ newAuthConnectionContext(),
+ newPatchRequest("/test1", add("/description/-", asList("one", "two"))));
}
@Test(expectedExceptions = BadRequestException.class)
public void testPatchMultiValuedAttributeWithSingleValue() throws Exception {
final Connection connection = newConnection();
- connection.patch(newAuthConnectionContext(), newPatchRequest("/test1", add("/description", "one")));
+
+ connection.patch(
+ newAuthConnectionContext(),
+ newPatchRequest("/test1", add("/description", "one")));
}
@Test
public void testPatchMVCCMatch() throws Exception {
final Connection connection = newConnection();
final Context context = newAuthConnectionContext();
- final ResourceResponse resource1 = connection.patch(
- context, newPatchRequest("/test1", add("/name/displayName", "changed")).setRevision("12345"));
+ final ResourceResponse resource1 =
+ connection.patch(
+ context,
+ newPatchRequest(
+ "/test1", add("/name/displayName", "changed")).setRevision("12345"));
+
checkResourcesAreEqual(resource1, getTestUser1Updated(12345));
+
final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
checkResourcesAreEqual(resource2, getTestUser1Updated(12345));
}
@@ -335,22 +649,27 @@
@Test(expectedExceptions = PreconditionFailedException.class)
public void testPatchMVCCNoMatch() throws Exception {
final Connection connection = newConnection();
+
connection.patch(
- newAuthConnectionContext(),
- newPatchRequest("/test1", add("/name/displayName", "changed")).setRevision("12346"));
+ newAuthConnectionContext(),
+ newPatchRequest(
+ "/test1",
+ add("/name/displayName", "changed")).setRevision("12346"));
}
@Test(expectedExceptions = NotFoundException.class)
public void testPatchNotFound() throws Exception {
newConnection().patch(
- newAuthConnectionContext(),
- newPatchRequest("/missing", add("/name/displayName", "changed")));
+ newAuthConnectionContext(),
+ newPatchRequest("/missing", add("/name/displayName", "changed")));
}
@Test(expectedExceptions = BadRequestException.class)
public void testPatchReadOnlyAttribute() throws Exception {
// Etag is read-only.
- newConnection().patch(newAuthConnectionContext(), newPatchRequest("/test1", add("_rev", "99999")));
+ newConnection().patch(
+ newAuthConnectionContext(),
+ newPatchRequest("/test1", add("_rev", "99999")));
}
@Test
@@ -363,9 +682,15 @@
field("_rev", "12345"),
field("name", object(field("displayName", "Humpty"),
field("surname", "Dumpty")))));
- final ResourceResponse resource1 = connection.patch(context, newPatchRequest("/test1",
- replace("/name", object(field("displayName", "Humpty"), field("surname", "Dumpty")))));
+
+ final ResourceResponse resource1 =
+ connection.patch(
+ context,
+ newPatchRequest("/test1",
+ replace("/name", object(field("displayName", "Humpty"),
+ field("surname", "Dumpty")))));
checkResourcesAreEqual(resource1, expected);
+
final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
checkResourcesAreEqual(resource2, expected);
}
@@ -374,18 +699,23 @@
public void testPatchReplaceWholeObject() throws Exception {
final Connection connection = newConnection();
final Context context = newAuthConnectionContext();
- final JsonValue newContent = json(object(
- field("name", object(field("displayName", "Humpty"),
- field("surname", "Dumpty")))));
- final JsonValue expected = json(object(
- field("schemas", asList("urn:scim:schemas:core:1.0")),
- field("_id", "test1"),
- field("_rev", "12345"),
- field("name", object(field("displayName", "Humpty"),
- field("surname", "Dumpty")))));
+ final JsonValue newContent =
+ json(object(
+ field("name", object(field("displayName", "Humpty"),
+ field("surname", "Dumpty")))));
+
+ final JsonValue expected =
+ json(object(
+ field("schemas", asList("urn:scim:schemas:core:1.0")),
+ field("_id", "test1"),
+ field("_rev", "12345"),
+ field("name", object(field("displayName", "Humpty"),
+ field("surname", "Dumpty")))));
+
final ResourceResponse resource1 =
- connection.patch(context, newPatchRequest("/test1", replace("/", newContent)));
+ connection.patch(context, newPatchRequest("/test1", replace("/", newContent)));
checkResourcesAreEqual(resource1, expected);
+
final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
checkResourcesAreEqual(resource2, expected);
}
@@ -393,32 +723,48 @@
@Test(expectedExceptions = BadRequestException.class)
public void testPatchSingleValuedAttributeIndexAppend() throws Exception {
final Connection connection = newConnection();
- connection.patch(newAuthConnectionContext(), newPatchRequest("/test1", add("/name/surname/-", "junk")));
+
+ connection.patch(
+ newAuthConnectionContext(),
+ newPatchRequest("/test1", add("/name/surname/-", "junk")));
}
@Test(expectedExceptions = NotSupportedException.class)
public void testPatchSingleValuedAttributeIndexNumber() throws Exception {
final Connection connection = newConnection();
- connection.patch(newAuthConnectionContext(), newPatchRequest("/test1", add("/name/surname/0", "junk")));
+
+ connection.patch(
+ newAuthConnectionContext(),
+ newPatchRequest("/test1", add("/name/surname/0", "junk")));
}
@Test(expectedExceptions = BadRequestException.class)
public void testPatchSingleValuedAttributeWithMultipleValues() throws Exception {
final Connection connection = newConnection();
- connection.patch(newAuthConnectionContext(), newPatchRequest("/test1", add("/name/surname", asList("black",
- "white"))));
+
+ connection.patch(
+ newAuthConnectionContext(),
+ newPatchRequest(
+ "/test1",
+ add("/name/surname", asList("black", "white"))));
}
@Test(expectedExceptions = BadRequestException.class)
public void testPatchUnknownAttribute() throws Exception {
final Connection connection = newConnection();
- connection.patch(newAuthConnectionContext(), newPatchRequest("/test1", add("/dummy", "junk")));
+
+ connection.patch(
+ newAuthConnectionContext(),
+ newPatchRequest("/test1", add("/dummy", "junk")));
}
@Test(expectedExceptions = BadRequestException.class)
public void testPatchUnknownSubAttribute() throws Exception {
final Connection connection = newConnection();
- connection.patch(newAuthConnectionContext(), newPatchRequest("/test1", add("/description/dummy", "junk")));
+
+ connection.patch(
+ newAuthConnectionContext(),
+ newPatchRequest("/test1", add("/description/dummy", "junk")));
}
@Test(expectedExceptions = BadRequestException.class)
@@ -430,7 +776,9 @@
@Test
public void testRead() throws Exception {
- final ResourceResponse resource = newConnection().read(newAuthConnectionContext(), newReadRequest("/test1"));
+ final ResourceResponse resource =
+ newConnection().read(newAuthConnectionContext(), newReadRequest("/test1"));
+
checkResourcesAreEqual(resource, getTestUser1(12345));
}
@@ -441,15 +789,19 @@
@Test
public void testReadSelectAllFields() throws Exception {
- final ResourceResponse resource = newConnection().read(newAuthConnectionContext(),
- newReadRequest("/test1").addField("/"));
+ final ResourceResponse resource =
+ newConnection().read(
+ newAuthConnectionContext(), newReadRequest("/test1").addField("/"));
+
checkResourcesAreEqual(resource, getTestUser1(12345));
}
@Test
public void testReadSelectPartial() throws Exception {
- final ResourceResponse resource = newConnection().read(
- newAuthConnectionContext(), newReadRequest("/test1").addField("/name/surname"));
+ final ResourceResponse resource =
+ newConnection().read(
+ newAuthConnectionContext(), newReadRequest("/test1").addField("/name/surname"));
+
assertThat(resource.getId()).isEqualTo("test1");
assertThat(resource.getRevision()).isEqualTo("12345");
assertThat(resource.getContent().get("_id").asString()).isNull();
@@ -461,8 +813,10 @@
/** Disabled - see CREST-86 (Should JSON resource fields be case insensitive?) */
@Test(enabled = false)
public void testReadSelectPartialInsensitive() throws Exception {
- final ResourceResponse resource = newConnection().read(
- newAuthConnectionContext(), newReadRequest("/test1").addField("/name/SURNAME"));
+ final ResourceResponse resource =
+ newConnection().read(
+ newAuthConnectionContext(), newReadRequest("/test1").addField("/name/SURNAME"));
+
assertThat(resource.getId()).isEqualTo("test1");
assertThat(resource.getRevision()).isEqualTo("12345");
assertThat(resource.getContent().get("_id").asString()).isNull();
@@ -476,8 +830,9 @@
final Connection connection = newConnection();
final Context context = newAuthConnectionContext();
final ResourceResponse resource1 = connection.update(
- context, newUpdateRequest("/test1", getTestUser1Updated(12345)));
+ context, newUpdateRequest("/test1", getTestUser1Updated(12345)));
checkResourcesAreEqual(resource1, getTestUser1Updated(12345));
+
final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
checkResourcesAreEqual(resource2, getTestUser1Updated(12345));
}
@@ -487,14 +842,15 @@
final List<Request> requests = new LinkedList<>();
final Connection connection = newConnection();
final Context context = newAuthConnectionContext(requests);
- final ResourceResponse resource1 = connection.update(context, newUpdateRequest("/test1", getTestUser1(12345)));
+ final ResourceResponse resource1 =
+ connection.update(context, newUpdateRequest("/test1", getTestUser1(12345)));
// Check that no modify operation was sent
// (only a single search should be sent in order to get the current resource).
assertThat(requests).hasSize(1);
assertThat(requests.get(0)).isInstanceOf(SearchRequest.class);
-
checkResourcesAreEqual(resource1, getTestUser1(12345));
+
final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
checkResourcesAreEqual(resource2, getTestUser1(12345));
}
@@ -504,9 +860,13 @@
final Connection connection = newConnection();
final Context context = newAuthConnectionContext();
final JsonValue newContent = getTestUser1Updated(12345);
+
newContent.put("description", asList("one", "two"));
- final ResourceResponse resource1 = connection.update(context, newUpdateRequest("/test1", newContent));
+
+ final ResourceResponse resource1 =
+ connection.update(context, newUpdateRequest("/test1", newContent));
checkResourcesAreEqual(resource1, newContent);
+
final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
checkResourcesAreEqual(resource2, newContent);
}
@@ -515,6 +875,7 @@
public void testUpdateConstantAttribute() throws Exception {
final Connection connection = newConnection();
final JsonValue newContent = getTestUser1Updated(12345);
+
newContent.put("schemas", asList("junk"));
connection.update(newAuthConnectionContext(), newUpdateRequest("/test1", newContent));
}
@@ -524,11 +885,15 @@
final Connection connection = newConnection();
final Context context = newAuthConnectionContext();
final JsonValue newContent = getTestUser1Updated(12345);
+
newContent.put("description", asList("one", "two"));
connection.update(newAuthConnectionContext(), newUpdateRequest("/test1", newContent));
newContent.remove("description");
- final ResourceResponse resource1 = connection.update(context, newUpdateRequest("/test1", newContent));
+
+ final ResourceResponse resource1 =
+ connection.update(context, newUpdateRequest("/test1", newContent));
checkResourcesAreEqual(resource1, newContent);
+
final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
checkResourcesAreEqual(resource2, newContent);
}
@@ -537,6 +902,7 @@
public void testUpdateMissingRequiredAttribute() throws Exception {
final Connection connection = newConnection();
final JsonValue newContent = getTestUser1Updated(12345);
+
newContent.get("name").remove("surname");
connection.update(newAuthConnectionContext(), newUpdateRequest("/test1", newContent));
}
@@ -546,11 +912,15 @@
final Connection connection = newConnection();
final Context context = newAuthConnectionContext();
final JsonValue newContent = getTestUser1Updated(12345);
+
newContent.put("description", asList("one", "two"));
connection.update(newAuthConnectionContext(), newUpdateRequest("/test1", newContent));
newContent.put("description", asList("three"));
- final ResourceResponse resource1 = connection.update(context, newUpdateRequest("/test1", newContent));
+
+ final ResourceResponse resource1 =
+ connection.update(context, newUpdateRequest("/test1", newContent));
checkResourcesAreEqual(resource1, newContent);
+
final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
checkResourcesAreEqual(resource2, newContent);
}
@@ -560,8 +930,11 @@
final Connection connection = newConnection();
final Context context = newAuthConnectionContext();
final ResourceResponse resource1 =
- connection.update(context, newUpdateRequest("/test1", getTestUser1Updated(12345)).setRevision("12345"));
+ connection.update(
+ context,
+ newUpdateRequest("/test1", getTestUser1Updated(12345)).setRevision("12345"));
checkResourcesAreEqual(resource1, getTestUser1Updated(12345));
+
final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
checkResourcesAreEqual(resource2, getTestUser1Updated(12345));
}
@@ -569,27 +942,36 @@
@Test(expectedExceptions = PreconditionFailedException.class)
public void testUpdateMVCCNoMatch() throws Exception {
final Connection connection = newConnection();
- connection.update(newAuthConnectionContext(), newUpdateRequest("/test1", getTestUser1Updated(12345))
- .setRevision("12346"));
+
+ connection.update(
+ newAuthConnectionContext(),
+ newUpdateRequest("/test1", getTestUser1Updated(12345)).setRevision("12346"));
}
@Test(expectedExceptions = NotFoundException.class)
public void testUpdateNotFound() throws Exception {
final Connection connection = newConnection();
- connection.update(newAuthConnectionContext(), newUpdateRequest("/missing", getTestUser1Updated(12345)));
+
+ connection.update(
+ newAuthConnectionContext(),
+ newUpdateRequest("/missing", getTestUser1Updated(12345)));
}
@Test(expectedExceptions = BadRequestException.class)
public void testUpdateReadOnlyAttribute() throws Exception {
final Connection connection = newConnection();
+
// Etag is read-only.
- connection.update(newAuthConnectionContext(), newUpdateRequest("/test1", getTestUser1Updated(99999)));
+ connection.update(
+ newAuthConnectionContext(),
+ newUpdateRequest("/test1", getTestUser1Updated(99999)));
}
@Test(expectedExceptions = BadRequestException.class)
public void testUpdateSingleValuedAttributeWithMultipleValues() throws Exception {
final Connection connection = newConnection();
final JsonValue newContent = getTestUser1Updated(12345);
+
newContent.put("surname", asList("black", "white"));
connection.update(newAuthConnectionContext(), newUpdateRequest("/test1", newContent));
}
@@ -598,6 +980,7 @@
public void testUpdateUnknownAttribute() throws Exception {
final Connection connection = newConnection();
final JsonValue newContent = getTestUser1Updated(12345);
+
newContent.add("dummy", "junk");
connection.update(newAuthConnectionContext(), newUpdateRequest("/test1", newContent));
}
@@ -607,40 +990,120 @@
}
private Rest2Ldap usersApi() throws IOException {
- return rest2Ldap(defaultOptions(),
- resource("api").subResource(collectionOf("user").dnTemplate("dc=test")
- .useClientDnNaming("uid")),
- resource("user").objectClasses("top", "person")
- .property("schemas", constant(asList("urn:scim:schemas:core:1.0")))
- .property("_id", simple("uid").isRequired(true).writability(CREATE_ONLY))
- .property("name", object().property("displayName",
- simple("cn").isRequired(true))
- .property("surname", simple("sn").isRequired(true)))
- .property("_rev", simple("etag").isRequired(true).writability(READ_ONLY))
- .property("description", simple("description").isMultiValued(true))
- .property("singleNumber",
- simple("singleNumber").decoder(byteStringToInteger()))
- .property("multiNumber",
- simple("multiNumber").isMultiValued(true)
- .decoder(byteStringToInteger())));
+ return rest2Ldap(
+ defaultOptions(),
+ resource("api")
+ .subResource(
+ collectionOf("user")
+ .dnTemplate("dc=test")
+ .useClientDnNaming("uid"))
+ .subResource(
+ collectionOf("user")
+ .urlTemplate("top-level-users")
+ .dnTemplate("dc=test")
+ .useClientDnNaming("uid")
+ .baseSearchFilter("(objectClass=person)"))
+ .subResource(
+ collectionOf("user")
+ .urlTemplate("all-entries")
+ .dnTemplate("dc=test")
+ .useClientDnNaming("uid")
+ .isReadOnly(true)
+ .flattenSubtree(true))
+ .subResource(
+ collectionOf("user")
+ .urlTemplate("all-users")
+ .dnTemplate("dc=test")
+ .useClientDnNaming("uid")
+ .isReadOnly(true)
+ .flattenSubtree(true)
+ .baseSearchFilter("(objectClass=person)")),
+ resource("user")
+ .objectClasses("top", "person")
+ .property(
+ "schemas",
+ constant(asList("urn:scim:schemas:core:1.0")))
+ .property(
+ "_id",
+ simple("uid").isRequired(true).writability(CREATE_ONLY))
+ .property(
+ "_ou",
+ simple("ou").isRequired(false).writability(CREATE_ONLY))
+ .property(
+ "name",
+ object()
+ .property("displayName", simple("cn").isRequired(true))
+ .property("surname", simple("sn").isRequired(true)))
+ .property(
+ "_rev",
+ simple("etag").isRequired(true).writability(READ_ONLY))
+ .property(
+ "description",
+ simple("description").isMultiValued(true))
+ .property(
+ "singleNumber",
+ simple("singleNumber").decoder(byteStringToInteger()))
+ .property(
+ "multiNumber",
+ simple("multiNumber").isMultiValued(true).decoder(byteStringToInteger()))
+ );
}
private void checkResourcesAreEqual(final ResourceResponse actual, final JsonValue expected) {
final ResourceResponse expectedResource = asResource(expected);
+
assertThat(actual.getId()).isEqualTo(expectedResource.getId());
assertThat(actual.getRevision()).isEqualTo(expectedResource.getRevision());
- assertThat(actual.getContent().getObject()).isEqualTo(expectedResource.getContent().getObject());
+
+ assertThat(actual.getContent().getObject())
+ .isEqualTo(expectedResource.getContent().getObject());
+ }
+
+ private void checkThatOrgUnitsExist(final List<ResourceResponse> resources,
+ final String... orgUnitIds) {
+ checkThatOrgUnitsExist(resources, 0, orgUnitIds);
+ }
+
+ private void checkThatOrgUnitsExist(final List<ResourceResponse> resources,
+ final int startingIndex,
+ final String... expectedOrgUnitIds) {
+ for (int orgUnitIndex = 0; orgUnitIndex < expectedOrgUnitIds.length; ++orgUnitIndex) {
+ final ResourceResponse resource = resources.get(startingIndex + orgUnitIndex);
+ final JsonValue orgUnitId = resource.getContent().get("_ou");
+
+ assertThat(orgUnitId).isNotNull();
+ assertThat(orgUnitId.asString()).isEqualTo(expectedOrgUnitIds[orgUnitIndex]);
+ }
+ }
+
+ private void checkThatUsersExist(final List<ResourceResponse> resources,
+ final String... expectedUserIds) {
+ checkThatUsersExist(resources, 0, expectedUserIds);
+ }
+
+ private void checkThatUsersExist(final List<ResourceResponse> resources,
+ final int startingIndex, final String... expectedUserIds) {
+ for (int userIndex = 0; userIndex < expectedUserIds.length; ++userIndex) {
+ final ResourceResponse resource = resources.get(startingIndex + userIndex);
+
+ assertThat(resource.getContent().get("_ou").isNull());
+ assertThat(resource.getId()).isEqualTo(expectedUserIds[userIndex]);
+ }
}
private AuthenticatedConnectionContext newAuthConnectionContext() throws IOException {
return newAuthConnectionContext(new ArrayList<Request>());
}
- private AuthenticatedConnectionContext newAuthConnectionContext(List<Request> requests) throws IOException {
- return new AuthenticatedConnectionContext(ctx(), getConnectionFactory(requests).getConnection());
+ private AuthenticatedConnectionContext newAuthConnectionContext(List<Request> requests)
+ throws IOException {
+ return new AuthenticatedConnectionContext(
+ ctx(),
+ getConnectionFactory(requests).getConnection());
}
- private ConnectionFactory getConnectionFactory(final List<Request> requests) throws IOException {
+ private ConnectionFactory getConnectionFactory(final List<Request> requests)
+ throws IOException {
// @formatter:off
final MemoryBackend backend =
new MemoryBackend(new LDIFEntryReader(
@@ -692,7 +1155,46 @@
"userpassword: password",
"cn: test user 5",
"sn: user 5",
- "etag: 55555"
+ "etag: 55555",
+ "",
+ "dn: uid=test6,dc=test",
+ "objectClass: top",
+ "objectClass: person",
+ "uid: test6",
+ "userpassword: password",
+ "cn: test user 6",
+ "sn: user 6",
+ "etag: 66666",
+ "",
+ "dn: ou=level1,dc=test",
+ "objectClass: top",
+ "objectClass: organizationalUnit",
+ "ou: level1",
+ "etag: 77777",
+ "",
+ "dn: uid=sub1,ou=level1,dc=test",
+ "objectClass: top",
+ "objectClass: person",
+ "uid: sub1",
+ "userpassword: password",
+ "cn: test user level 1",
+ "sn: user 7",
+ "etag: 88888",
+ "",
+ "dn: ou=level2,ou=level1,dc=test",
+ "objectClass: top",
+ "objectClass: organizationalUnit",
+ "ou: level2",
+ "etag: 99999",
+ "",
+ "dn: uid=sub2,ou=level2,ou=level1,dc=test",
+ "objectClass: top",
+ "objectClass: person",
+ "uid: sub2",
+ "userpassword: password",
+ "cn: test user level 2",
+ "sn: user 8",
+ "etag: 86753"
));
// @formatter:on
@@ -768,10 +1270,15 @@
@Override
public void handleSearch(RequestContext requestContext, SearchRequest request,
- IntermediateResponseHandler intermediateResponseHandler, SearchResultHandler entryHandler,
+ IntermediateResponseHandler intermediateResponseHandler,
+ SearchResultHandler entryHandler,
LdapResultHandler<Result> resultHandler) {
requests.add(request);
- handler.handleSearch(requestContext, request, intermediateResponseHandler, entryHandler,
+ handler.handleSearch(
+ requestContext,
+ request,
+ intermediateResponseHandler,
+ entryHandler,
resultHandler);
}
diff --git a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/OAuth2JsonConfigurationTestCase.java b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/OAuth2JsonConfigurationTestCase.java
index 98b08f0..0f0785a 100644
--- a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/OAuth2JsonConfigurationTestCase.java
+++ b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/OAuth2JsonConfigurationTestCase.java
@@ -12,6 +12,7 @@
* information: "Portions Copyright [year] [name of copyright owner]".
*
* Copyright 2016 ForgeRock AS.
+ * Portions Copyright 2017 Rosie Applications, Inc.
*/
package org.forgerock.opendj.rest2ldap;
@@ -190,7 +191,7 @@
}
@DataProvider
- public Object[][] ingnoredCacheResolverConfigurations() {
+ public Object[][] ignoredCacheResolverConfigurations() {
// @Checkstyle:off
return new Object[][] {
{
@@ -205,7 +206,7 @@
// @Checkstyle:on
}
- @Test(dataProvider = "ingnoredCacheResolverConfigurations")
+ @Test(dataProvider = "ignoredCacheResolverConfigurations")
public void testNoCacheFallbackOnResolver(final String rawJson) throws Exception {
assertThat(fakeApp.createCachedTokenResolverIfNeeded(parseJson(rawJson), resolver)).isEqualTo(resolver);
}
diff --git a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfiguratorTest.java b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfiguratorTest.java
index 2690bd4..f948f30 100644
--- a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfiguratorTest.java
+++ b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfiguratorTest.java
@@ -12,6 +12,7 @@
* information: "Portions Copyright [year] [name of copyright owner]".
*
* Copyright 2016 ForgeRock AS.
+ * Portions Copyright 2017 Rosie Applications, Inc.
*/
package org.forgerock.opendj.rest2ldap;
@@ -23,22 +24,30 @@
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;
import java.util.Collections;
+import java.util.List;
+import java.util.Map;
import org.forgerock.api.CrestApiProducer;
import org.forgerock.api.models.ApiDescription;
+import org.forgerock.api.models.Items;
+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;
import org.forgerock.json.resource.Request;
import org.forgerock.json.resource.RequestHandler;
+import org.forgerock.opendj.ldap.Filter;
import org.forgerock.services.context.Context;
import org.forgerock.services.context.RootContext;
import org.forgerock.testng.ForgeRockTestCase;
import org.forgerock.util.Options;
+import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -53,13 +62,19 @@
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 File endpointsDir = CONFIG_DIR.resolve("endpoints").toFile();
+ final DescribableRequestHandler handler = createDescribableHandler(endpointsDir);
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 +82,401 @@
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/all-users",
+ "/api/groups");
+
assertThat(api.getDefinitions().getNames()).containsOnly(
"frapi:opendj:rest2ldap:object:1.0",
"frapi:opendj:rest2ldap:group:1.0",
"frapi:opendj:rest2ldap:user:1.0",
"frapi:opendj:rest2ldap:posixUser:1.0");
+
+ final Services services = api.getServices();
+
+ assertThat(services.getNames()).containsOnly(
+ "frapi:opendj:rest2ldap:user:1.0:read-write",
+ "frapi:opendj:rest2ldap:user:1.0:read-only",
+ "frapi:opendj:rest2ldap:group:1.0:read-write");
+
+ final String[] readOnlyServices = new String[] {
+ "frapi:opendj:rest2ldap:user:1.0:read-only"
+ };
+
+ for (String serviceName : readOnlyServices) {
+ final Resource service = services.get(serviceName);
+ final Items items = service.getItems();
+
+ assertThat(service.getCreate()).isNull();
+ assertThat(items.getCreate()).isNull();
+ assertThat(items.getUpdate()).isNull();
+ assertThat(items.getDelete()).isNull();
+ assertThat(items.getPatch()).isNull();
+
+ assertThat(items.getRead()).isNotNull();
+ }
+
+ final String[] writableServices = new String[] {
+ "frapi:opendj:rest2ldap:user:1.0:read-write",
+ "frapi:opendj:rest2ldap:group:1.0:read-write"
+ };
+
+ for (String serviceName : writableServices) {
+ final Resource service = services.get(serviceName);
+ final Items items = service.getItems();
+
+ assertThat(service.getCreate()).isNotNull();
+ assertThat(items.getCreate()).isNotNull();
+ assertThat(items.getUpdate()).isNotNull();
+ assertThat(items.getDelete()).isNotNull();
+ assertThat(items.getPatch()).isNotNull();
+ assertThat(items.getRead()).isNotNull();
+ }
}
- private DescribableRequestHandler configureEndpoints(final File endpointsDir) throws Exception {
- final RequestHandler rh = Rest2LdapJsonConfigurator.configureEndpoints(endpointsDir, Options.defaultOptions());
- DescribableRequestHandler handler = new DescribableRequestHandler(rh);
+ @DataProvider
+ public Object[][] invalidSubResourceSubtreeFlatteningConfigurations() {
+ // @Checkstyle:off
+ return new Object[][] {
+ {
+ "{"
+ + "'example-v1': {"
+ + "'subResources': {"
+ + "'writeable-collection': {"
+ + "'type': 'collection',"
+ + "'dnTemplate': 'ou=people,dc=example,dc=com',"
+ + "'resource': 'frapi:opendj:rest2ldap:user:1.0',"
+ + "'namingStrategy': {"
+ + "'type': 'clientDnNaming',"
+ + "'dnAttribute': 'uid'"
+ + "},"
+ + "'flattenSubtree': true"
+ + "}"
+ + "}"
+ + "}"
+ + "}"
+ },
+ {
+ "{"
+ + "'example-v1': {"
+ + "'subResources': {"
+ + "'writeable-collection': {"
+ + "'type': 'collection',"
+ + "'dnTemplate': 'ou=people,dc=example,dc=com',"
+ + "'resource': 'frapi:opendj:rest2ldap:user:1.0',"
+ + "'namingStrategy': {"
+ + "'type': 'clientDnNaming',"
+ + "'dnAttribute': 'uid'"
+ + "},"
+ + "'isReadOnly': false,"
+ + "'flattenSubtree': true"
+ + "}"
+ + "}"
+ + "}"
+ + "}"
+ }
+ };
+ // @Checkstyle:on
+ }
+
+ @DataProvider
+ public Object[][] validSubResourceConfigurations() {
+ // @Checkstyle:off
+ return new Object[][] {
+ {
+ false,
+ false,
+ null,
+ "{"
+ + "'example-v1': {"
+ + "'subResources': {"
+ + "'all-users': {"
+ + "'type': 'collection',"
+ + "'dnTemplate': 'ou=people,dc=example,dc=com',"
+ + "'resource': 'frapi:opendj:rest2ldap:user:1.0',"
+ + "'namingStrategy': {"
+ + "'type': 'clientDnNaming',"
+ + "'dnAttribute': 'uid'"
+ + "}"
+ + "}"
+ + "}"
+ + "}"
+ + "}"
+ },
+ {
+ false,
+ false,
+ "(objectClass=person)",
+ "{"
+ + "'example-v1': {"
+ + "'subResources': {"
+ + "'all-users': {"
+ + "'type': 'collection',"
+ + "'dnTemplate': 'ou=people,dc=example,dc=com',"
+ + "'resource': 'frapi:opendj:rest2ldap:user:1.0',"
+ + "'namingStrategy': {"
+ + "'type': 'clientDnNaming',"
+ + "'dnAttribute': 'uid'"
+ + "},"
+ + "'baseSearchFilter': '(objectClass=person)'"
+ + "}"
+ + "}"
+ + "}"
+ + "}"
+ },
+ {
+ false,
+ false,
+ null,
+ "{"
+ + "'example-v1': {"
+ + "'subResources': {"
+ + "'all-users': {"
+ + "'type': 'collection',"
+ + "'dnTemplate': 'ou=people,dc=example,dc=com',"
+ + "'resource': 'frapi:opendj:rest2ldap:user:1.0',"
+ + "'namingStrategy': {"
+ + "'type': 'clientDnNaming',"
+ + "'dnAttribute': 'uid'"
+ + "},"
+ + "'flattenSubtree': false"
+ + "}"
+ + "}"
+ + "}"
+ + "}"
+ },
+ {
+ true,
+ false,
+ null,
+ "{"
+ + "'example-v1': {"
+ + "'subResources': {"
+ + "'all-users': {"
+ + "'type': 'collection',"
+ + "'dnTemplate': 'ou=people,dc=example,dc=com',"
+ + "'resource': 'frapi:opendj:rest2ldap:user:1.0',"
+ + "'namingStrategy': {"
+ + "'type': 'clientDnNaming',"
+ + "'dnAttribute': 'uid'"
+ + "},"
+ + "'isReadOnly': true"
+ + "}"
+ + "}"
+ + "}"
+ + "}"
+ },
+ {
+ true,
+ false,
+ null,
+ "{"
+ + "'example-v1': {"
+ + "'subResources': {"
+ + "'all-users': {"
+ + "'type': 'collection',"
+ + "'dnTemplate': 'ou=people,dc=example,dc=com',"
+ + "'resource': 'frapi:opendj:rest2ldap:user:1.0',"
+ + "'namingStrategy': {"
+ + "'type': 'clientDnNaming',"
+ + "'dnAttribute': 'uid'"
+ + "},"
+ + "'isReadOnly': true,"
+ + "'flattenSubtree': false"
+ + "}"
+ + "}"
+ + "}"
+ + "}"
+ },
+ {
+ false,
+ false,
+ null,
+ "{"
+ + "'example-v1': {"
+ + "'subResources': {"
+ + "'all-users': {"
+ + "'type': 'collection',"
+ + "'dnTemplate': 'ou=people,dc=example,dc=com',"
+ + "'resource': 'frapi:opendj:rest2ldap:user:1.0',"
+ + "'namingStrategy': {"
+ + "'type': 'clientDnNaming',"
+ + "'dnAttribute': 'uid'"
+ + "},"
+ + "'isReadOnly': false,"
+ + "'flattenSubtree': false"
+ + "}"
+ + "}"
+ + "}"
+ + "}"
+ },
+ {
+ true,
+ true,
+ null,
+ "{"
+ + "'example-v1': {"
+ + "'subResources': {"
+ + "'all-users': {"
+ + "'type': 'collection',"
+ + "'dnTemplate': 'ou=people,dc=example,dc=com',"
+ + "'resource': 'frapi:opendj:rest2ldap:user:1.0',"
+ + "'namingStrategy': {"
+ + "'type': 'clientDnNaming',"
+ + "'dnAttribute': 'uid'"
+ + "},"
+ + "'isReadOnly': true,"
+ + "'flattenSubtree': true"
+ + "}"
+ + "}"
+ + "}"
+ + "}"
+ }
+ };
+ // @Checkstyle:on
+ }
+
+ @Test(dataProvider = "invalidSubResourceSubtreeFlatteningConfigurations")
+ public void testInvalidSubResourceSubtreeFlatteningConfigurations(final String rawJson)
+ throws Exception {
+ try {
+ Rest2LdapJsonConfigurator.configureResources(parseJson(rawJson));
+
+ fail("Expected an IllegalArgumentException");
+ }
+ catch (IllegalArgumentException ex) {
+ assertThat(ex.getMessage())
+ .isEqualTo("Sub-resources must be read-only to support sub-tree flattening.");
+ }
+ }
+
+ @Test
+ public void testInvalidSubResourceSearchFilterConfiguration()
+ throws Exception {
+ final String rawJson =
+ "{"
+ + "'example-v1': {"
+ + "'subResources': {"
+ + "'all-users': {"
+ + "'type': 'collection',"
+ + "'dnTemplate': 'ou=people,dc=example,dc=com',"
+ + "'resource': 'frapi:opendj:rest2ldap:user:1.0',"
+ + "'namingStrategy': {"
+ + "'type': 'clientDnNaming',"
+ + "'dnAttribute': 'uid'"
+ + "},"
+ + "'baseSearchFilter': 'badFilter'"
+ + "}"
+ + "}"
+ + "}"
+ + "}";
+
+ try {
+ Rest2LdapJsonConfigurator.configureResources(parseJson(rawJson));
+
+ fail("Expected an IllegalArgumentException");
+ }
+ catch (IllegalArgumentException ex) {
+ assertThat(ex.getMessage())
+ .isEqualTo(
+ "The provided search filter \"badFilter\" was missing an equal sign in the " +
+ "suspected simple filter component between positions 0 and 9");
+ }
+ }
+
+ @Test(dataProvider = "validSubResourceConfigurations")
+ public void testValidSubResourceConfigurations(final boolean expectedReadOnly,
+ final boolean expectedSubtreeFlattened,
+ final String expectedSearchFilter,
+ final String rawJson) throws Exception {
+ final List<org.forgerock.opendj.rest2ldap.Resource> resources =
+ Rest2LdapJsonConfigurator.configureResources(parseJson(rawJson));
+ final org.forgerock.opendj.rest2ldap.Resource firstResource;
+ final Map<String, SubResource> subResources;
+ final SubResourceCollection allUsersSubResource;
+
+ assertThat(resources.size()).isEqualTo(1);
+
+ firstResource = resources.get(0);
+
+ assertThat(firstResource.getResourceId()).isEqualTo("example-v1");
+
+ subResources = firstResource.getSubResourceMap();
+
+ assertThat(subResources.size()).isEqualTo(1);
+
+ allUsersSubResource = (SubResourceCollection)subResources.get("all-users");
+
+ assertThat(allUsersSubResource.isReadOnly()).isEqualTo(expectedReadOnly);
+ assertThat(allUsersSubResource.shouldFlattenSubtree()).isEqualTo(expectedSubtreeFlattened);
+
+ if (expectedSearchFilter == null) {
+ assertThat(allUsersSubResource.getBaseSearchFilter()).isNull();
+ }
+ else {
+ assertThat(allUsersSubResource.getBaseSearchFilter().toString())
+ .isEqualTo(expectedSearchFilter);
+ }
+ }
+
+ private RequestHandler createRequestHandler(final File endpointsDir) throws IOException {
+ 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);
+ }
}
diff --git a/opendj-server-legacy/resource/config/rest2ldap/endpoints/api/example-v1.json b/opendj-server-legacy/resource/config/rest2ldap/endpoints/api/example-v1.json
index 096de36..0bb2c83 100644
--- a/opendj-server-legacy/resource/config/rest2ldap/endpoints/api/example-v1.json
+++ b/opendj-server-legacy/resource/config/rest2ldap/endpoints/api/example-v1.json
@@ -21,6 +21,34 @@
"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
+ },
+ // This resource provides a read-only view of all users in the system, including
+ // users nested underneath entries like org units, organizations, etc., starting
+ // from "ou=people,dc=example,dc=com" and working down. It filters out any other
+ // structural elements, including organizations, org units, etc.
+ "all-users": {
+ "type": "collection",
+ "dnTemplate": "ou=people,dc=example,dc=com",
+ "resource": "frapi:opendj:rest2ldap:user:1.0",
+ "namingStrategy": {
+ "type": "clientDnNaming",
+ "dnAttribute": "uid"
+ },
+ "isReadOnly": true,
+ "flattenSubtree": true,
+ "baseSearchFilter": "(objectClass=person)"
+ },
"groups": {
"type": "collection",
"dnTemplate": "ou=groups,dc=example,dc=com",
--
Gitblit v1.10.0