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", 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); } } } 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,12 +91,10 @@ } /** * 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. * The filter which should be used when searching for referenced LDAP entries. * @return This property mapper. */ public ReferencePropertyMapper searchFilter(final Filter filter) { @@ -104,11 +104,10 @@ /** * 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. * The filter which should be used when searching for referenced LDAP entries. * @return This property mapper. */ public ReferencePropertyMapper searchFilter(final String filter) { @@ -116,8 +115,8 @@ } /** * 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 @@ -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"); } 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)); 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(); } 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) { 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,24 +306,59 @@ 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") final SubResourceType subResourceType = config.get("type").required().as(enumConstant(SubResourceType.class)); if (subResourceType == SubResourceType.COLLECTION) { return configureCollectionSubResource( config, resourceId, urlTemplate, dnTemplate, isReadOnly); } else { 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 SubResourceCollection collection = collectionOf(resourceId).urlTemplate(urlTemplate) 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); .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(); switch (namingStrategy.get("type").required().as(enumConstant(NamingStrategyType.class))) { final NamingStrategyType namingStrategyType = namingStrategy.get("type").required().as(enumConstant(NamingStrategyType.class)); switch (namingStrategyType) { case CLIENTDNNAMING: collection.useClientDnNaming(namingStrategy.get("dnAttribute").required().asString()); break; @@ -336,11 +371,17 @@ namingStrategy.get("idAttribute").required().asString()); break; } return collection; } else { return singletonOf(resourceId).urlTemplate(urlTemplate).dnTemplate(dnTemplate).isReadOnly(isReadOnly); } 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) { 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; } 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, return new SubResourceImpl( rest2Ldap, dnFrom(context), dnTemplateString.isEmpty() ? null : glueObjectClasses, namingStrategy, resource); resource, flattenSubtree, baseSearchFilter); } private String idFrom(final Context context) { 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>() { 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); } /** 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. 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", 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,9 +649,12 @@ @Test(expectedExceptions = PreconditionFailedException.class) public void testPatchMVCCNoMatch() throws Exception { final Connection connection = newConnection(); connection.patch( newAuthConnectionContext(), newPatchRequest("/test1", add("/name/displayName", "changed")).setRevision("12346")); newPatchRequest( "/test1", add("/name/displayName", "changed")).setRevision("12346")); } @Test(expectedExceptions = NotFoundException.class) @@ -350,7 +667,9 @@ @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( final JsonValue newContent = json(object( field("name", object(field("displayName", "Humpty"), field("surname", "Dumpty"))))); final JsonValue expected = json(object( 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))); 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( 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( 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(); @@ -478,6 +832,7 @@ final ResourceResponse resource1 = connection.update( 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)) 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", .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()))); .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); } 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); } 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(); } private DescribableRequestHandler configureEndpoints(final File endpointsDir) throws Exception { final RequestHandler rh = Rest2LdapJsonConfigurator.configureEndpoints(endpointsDir, Options.defaultOptions()); DescribableRequestHandler handler = new DescribableRequestHandler(rh); final String[] writableServices = new String[] { "frapi:opendj:rest2ldap:user:1.0:read-write", "frapi:opendj:rest2ldap:group:1.0:read-write" }; for (String serviceName : writableServices) { final Resource service = services.get(serviceName); final Items items = service.getItems(); assertThat(service.getCreate()).isNotNull(); assertThat(items.getCreate()).isNotNull(); assertThat(items.getUpdate()).isNotNull(); assertThat(items.getDelete()).isNotNull(); assertThat(items.getPatch()).isNotNull(); assertThat(items.getRead()).isNotNull(); } } @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); } } 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",