Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,22 @@
},
"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,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;

Expand Down Expand Up @@ -61,6 +61,7 @@ protected <V> Promise<V, ResourceException> handleRequest(final Context context,
}

@Override
@SuppressWarnings("unchecked")
public ApiDescription api(ApiProducer<ApiDescription> producer) {
if (delegate instanceof Describable) {
return ((Describable<ApiDescription, Request>)delegate).api(producer);
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 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 @@ -484,7 +487,7 @@ String getResourceId() {
* @return The unique service ID for this resource, given the specified writability.
*/
String getServiceId(boolean isReadOnly) {
StringBuilder serviceId = new StringBuilder(this.getResourceId());
final StringBuilder serviceId = new StringBuilder(this.getResourceId());

if (isReadOnly) {
serviceId.append(":read-only");
Expand All @@ -495,6 +498,21 @@ String getServiceId(boolean isReadOnly) {
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
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
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -306,43 +306,84 @@ private static Resource configureResource(final String resourceId, final JsonVal
private enum NamingStrategyType { CLIENTDNNAMING, CLIENTNAMING, SERVERNAMING }
private enum SubResourceType { COLLECTION, SINGLETON }

private static SubResource configureSubResource(final String urlTemplate, final JsonValue config) {
private static SubResource configureSubResource(final String urlTemplate,
final JsonValue config) {
final String dnTemplate = config.get("dnTemplate").defaultTo("").asString();
final Boolean isReadOnly = config.get("isReadOnly").defaultTo(false).asBoolean();
final String resourceId = config.get("resource").required().asString();

if (config.get("type").required().as(enumConstant(SubResourceType.class)) == SubResourceType.COLLECTION) {
final String[] glueObjectClasses = config.get("glueObjectClasses")
.defaultTo(emptyList())
.asList(String.class)
.toArray(new String[0]);

final SubResourceCollection collection = collectionOf(resourceId).urlTemplate(urlTemplate)
.dnTemplate(dnTemplate)
.isReadOnly(isReadOnly)
.glueObjectClasses(glueObjectClasses);

final JsonValue namingStrategy = config.get("namingStrategy").required();
switch (namingStrategy.get("type").required().as(enumConstant(NamingStrategyType.class))) {
case CLIENTDNNAMING:
collection.useClientDnNaming(namingStrategy.get("dnAttribute").required().asString());
break;
case CLIENTNAMING:
collection.useClientNaming(namingStrategy.get("dnAttribute").required().asString(),
namingStrategy.get("idAttribute").required().asString());
break;
case SERVERNAMING:
collection.useServerNaming(namingStrategy.get("dnAttribute").required().asString(),
namingStrategy.get("idAttribute").required().asString());
break;
}
final SubResourceType subResourceType =
config.get("type").required().as(enumConstant(SubResourceType.class));

return collection;
if (subResourceType == SubResourceType.COLLECTION) {
return configureCollectionSubResource(
config, resourceId, urlTemplate, dnTemplate, isReadOnly);
} else {
return singletonOf(resourceId).urlTemplate(urlTemplate).dnTemplate(dnTemplate).isReadOnly(isReadOnly);
return configureSingletonSubResource(
config, resourceId, urlTemplate, dnTemplate, isReadOnly);
}
}

private static SubResource configureCollectionSubResource(final JsonValue config,
final String resourceId,
final String urlTemplate,
final String dnTemplate,
final Boolean isReadOnly) {
final String[] glueObjectClasses =
config.get("glueObjectClasses")
.defaultTo(emptyList())
.asList(String.class)
.toArray(new String[0]);

final Boolean flattenSubtree = config.get("flattenSubtree").defaultTo(false).asBoolean();
final String searchFilter = config.get("baseSearchFilter").asString();

final SubResourceCollection collection =
collectionOf(resourceId)
.urlTemplate(urlTemplate)
.dnTemplate(dnTemplate)
.isReadOnly(isReadOnly)
.glueObjectClasses(glueObjectClasses)
.flattenSubtree(flattenSubtree)
.baseSearchFilter(searchFilter);

configureCollectionNamingStrategy(config, collection);

return collection;
}

private static void configureCollectionNamingStrategy(final JsonValue config,
final SubResourceCollection collection) {
final JsonValue namingStrategy = config.get("namingStrategy").required();
final NamingStrategyType namingStrategyType =
namingStrategy.get("type").required().as(enumConstant(NamingStrategyType.class));

switch (namingStrategyType) {
case CLIENTDNNAMING:
collection.useClientDnNaming(namingStrategy.get("dnAttribute").required().asString());
break;
case CLIENTNAMING:
collection.useClientNaming(namingStrategy.get("dnAttribute").required().asString(),
namingStrategy.get("idAttribute").required().asString());
break;
case SERVERNAMING:
collection.useServerNaming(namingStrategy.get("dnAttribute").required().asString(),
namingStrategy.get("idAttribute").required().asString());
break;
}
}

private static SubResource configureSingletonSubResource(final JsonValue config,
final String resourceId,
final String urlTemplate,
final String dnTemplate,
final Boolean isReadOnly) {
return singletonOf(resourceId)
.urlTemplate(urlTemplate)
.dnTemplate(dnTemplate)
.isReadOnly(isReadOnly);
}

private static PropertyMapper configurePropertyMapper(final JsonValue mapper, final String defaultLdapAttribute) {
switch (mapper.get("type").required().asString()) {
case "resourceType":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -60,9 +60,10 @@ public abstract class SubResource {

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;
Expand All @@ -80,9 +81,27 @@ public final int hashCode() {

@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;
}
Expand Down
Loading