Skip to content

Commit

Permalink
Merge pull request #3 from GuyPaddock/wren/feature/subtree-flattening
Browse files Browse the repository at this point in the history
Wren/feature/subtree flattening
  • Loading branch information
vharseko authored Nov 22, 2017
2 parents c13258e + b8f565b commit a2d67f6
Show file tree
Hide file tree
Showing 15 changed files with 1,430 additions and 204 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

/**
Expand Down Expand Up @@ -56,4 +59,15 @@ public Promise<ResourceResponse, ResourceException> handleRead(
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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -82,20 +83,19 @@ public final class ReferencePropertyMapper extends AbstractLdapPropertyMapper<Re
final String baseDnTemplate, final AttributeDescription primaryKey,
final PropertyMapper mapper) {
super(ldapAttributeName);

this.schema = schema;
this.baseDnTemplate = DnTemplate.compile(baseDnTemplate);
this.primaryKey = primaryKey;
this.mapper = mapper;
}

/**
* 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);
Expand All @@ -104,25 +104,24 @@ public ReferencePropertyMapper searchFilter(final Filter filter) {

/**
* 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);
Expand All @@ -142,9 +141,9 @@ Promise<Filter, ResourceException> getLdapFilter(final Context context, final Re
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() {
Expand Down Expand Up @@ -325,8 +324,9 @@ public JsonValue apply(final List<JsonValue> value) {
}
}

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");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand Down Expand Up @@ -470,6 +473,46 @@ String getResourceId() {
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) {
Expand Down Expand Up @@ -522,7 +565,7 @@ ApiDescription instanceApi(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());
Expand All @@ -539,8 +582,8 @@ ApiDescription instanceApi(boolean isReadOnly) {
return ApiDescription.apiDescription()
.id("unused").version("unused")
.definitions(definitions())
.services(services(resource))
.paths(paths())
.services(services(resource, isReadOnly))
.paths(paths(isReadOnly))
.errors(errors())
.build();
}
Expand All @@ -555,13 +598,17 @@ ApiDescription instanceApi(boolean isReadOnly) {
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)
Expand All @@ -580,23 +627,29 @@ ApiDescription collectionApi(boolean isReadOnly) {
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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -371,7 +372,7 @@ private AccessTokenResolver parseRfc7662Resolver(final JsonValue configuration)
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);
}
}
Expand All @@ -394,8 +395,8 @@ private Duration parseCacheExpiration(final JsonValue expirationJson) {
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) {
Expand Down
Loading

0 comments on commit a2d67f6

Please sign in to comment.