mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Matthew Swift
08.50.2016 65f47d9c24da91fdeac1eb6f012f44cc056ac4e4
OPENDJ-3160 Support DN templates in reference property base DNs

Reference property mapper base DNs may include template parameters and
relative parent RDNs of the form "..". For example, the DN
"ou=groups,..,.." refers to the entry with RDN ou=groups residing
immediately subordinate to the grandparent of this resource's LDAP
entry.
8 files modified
136 ■■■■■ changed files
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/DnTemplate.java 13 ●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferencePropertyMapper.java 17 ●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2Ldap.java 23 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/RoutingContext.java 16 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceCollection.java 1 ●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceImpl.java 64 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceSingleton.java 1 ●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/DnTemplateTest.java 1 ●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/DnTemplate.java
@@ -44,10 +44,11 @@
 * </table>
 */
final class DnTemplate {
    private static final Pattern TEMPLATE_KEY_RE = Pattern.compile("\\{([^}]+)\\}");
    private static final Pattern TEMPLATE_VARIABLE_RE = Pattern.compile("\\{([^}]+)\\}");
    private final String template;
    private final String formatString;
    private final List<String> variables;
    /** A value of -1 means that this DN template is absolute. */
    private final int relativeOffset;
    /**
@@ -65,8 +66,7 @@
    /**
     * Compiles a DN template which will resolve LDAP entries relative to the root DSE by default, but MAY include
     * relative RDNs indicating that that the DN template will be resolved against current routing state
     * instead.
     * relative RDNs indicating that the DN template will be resolved against current routing state instead.
     *
     * @param template
     *         The string representation of the DN template.
@@ -100,7 +100,7 @@
        }
        final List<String> templateVariables = new ArrayList<>();
        final Matcher matcher = TEMPLATE_KEY_RE.matcher(trimmedTemplate);
        final Matcher matcher = TEMPLATE_VARIABLE_RE.matcher(trimmedTemplate);
        final StringBuffer buffer = new StringBuffer(trimmedTemplate.length());
        while (matcher.find()) {
            matcher.appendReplacement(buffer, "%s");
@@ -121,7 +121,8 @@
        // First determine the base DN based on the context DN and the relative offset.
        DN baseDn = null;
        if (relativeOffset >= 0 && context.containsContext(RoutingContext.class)) {
            baseDn = context.asContext(RoutingContext.class).getDn().parent(relativeOffset);
            final RoutingContext routingContext = context.asContext(RoutingContext.class);
            baseDn = routingContext.getDn().parent(routingContext.isCollection() ? relativeOffset - 1 : relativeOffset);
        }
        if (baseDn == null) {
            baseDn = DN.rootDN();
@@ -152,7 +153,7 @@
                return value;
            }
            if (!uriRouterContext.getParent().containsContext(UriRouterContext.class)) {
                throw new IllegalStateException("DN template parameter " + parameter + " cannot be resolved");
                throw new IllegalStateException("DN template parameter \"" + parameter + "\" cannot be resolved");
            }
            uriRouterContext = uriRouterContext.getParent().asContext(UriRouterContext.class);
        }
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferencePropertyMapper.java
@@ -72,18 +72,19 @@
     */
    private static final int SEARCH_MAX_CANDIDATES = 1000;
    private final DN baseDn;
    private final DnTemplate baseDnTemplate;
    private final Schema schema;
    private Filter filter;
    private final PropertyMapper mapper;
    private final AttributeDescription primaryKey;
    private SearchScope scope = SearchScope.WHOLE_SUBTREE;
    ReferencePropertyMapper(final Schema schema, final AttributeDescription ldapAttributeName, final DN baseDn,
                            final AttributeDescription primaryKey, final PropertyMapper mapper) {
    ReferencePropertyMapper(final Schema schema, final AttributeDescription ldapAttributeName,
                            final String baseDnTemplate, final AttributeDescription primaryKey,
                            final PropertyMapper mapper) {
        super(ldapAttributeName);
        this.schema = schema;
        this.baseDn = baseDn;
        this.baseDnTemplate = DnTemplate.compile(baseDnTemplate);
        this.primaryKey = primaryKey;
        this.mapper = mapper;
    }
@@ -144,7 +145,7 @@
                    @Override
                    public Promise<Filter, ResourceException> apply(final Filter result) {
                        // Search for all referenced entries and construct a filter.
                        final SearchRequest request = createSearchRequest(result);
                        final SearchRequest request = createSearchRequest(context, result);
                        final List<Filter> subFilters = new LinkedList<>();
                        return connectionFrom(context).searchAsync(request, new SearchResultHandler() {
@@ -224,7 +225,7 @@
                          // Now search for the referenced entry in to get its DN.
                          final ByteString primaryKeyValue = primaryKeyAttribute.firstValue();
                          final Filter filter = Filter.equality(primaryKey.toString(), primaryKeyValue);
                          final SearchRequest search = createSearchRequest(filter);
                          final SearchRequest search = createSearchRequest(context, filter);
                          connectionFrom(context).searchSingleEntryAsync(search)
                                    .thenOnResult(new ResultHandler<SearchResultEntry>() {
                                        @Override
@@ -325,9 +326,9 @@
        }
    }
    private SearchRequest createSearchRequest(final Filter result) {
    private SearchRequest createSearchRequest(final Context context, final Filter result) {
        final Filter searchFilter = filter != null ? Filter.and(filter, result) : result;
        return newSearchRequest(baseDn, scope, searchFilter, "1.1");
        return newSearchRequest(baseDnTemplate.format(context), scope, searchFilter, "1.1");
    }
    private Promise<JsonValue, ResourceException> readEntry(
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2Ldap.java
@@ -54,7 +54,6 @@
import org.forgerock.opendj.ldap.AuthorizationException;
import org.forgerock.opendj.ldap.ConnectionException;
import org.forgerock.opendj.ldap.ConstraintViolationException;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.DecodeOptions;
import org.forgerock.opendj.ldap.EntryNotFoundException;
import org.forgerock.opendj.ldap.LdapException;
@@ -244,18 +243,21 @@
     *
     * @param attribute
     *         The DN valued LDAP attribute to be mapped.
     * @param baseDN
     *         The search base DN for performing reverse lookups.
     * @param baseDnTemplate
     *         The DN template which will be used as the search base when performing reverse lookups. The DN template
     *         may include template parameters and also parent RDNs using ".." notation. For example, the DN template
     *         "ou=groups,..,.." specifies that the search base DN should be computed by appending the RDN
     *         "ou=groups" to the grand-parent of the current resource's LDAP entry.
     * @param primaryKey
     *         The search primary key LDAP attribute to use for performing reverse lookups.
     * @param mapper
     *         An property mapper which will be used to map LDAP attributes in the referenced entry.
     * @return The property mapper.
     */
    public static ReferencePropertyMapper reference(final AttributeDescription attribute, final DN baseDN,
    public static ReferencePropertyMapper reference(final AttributeDescription attribute, final String baseDnTemplate,
                                                    final AttributeDescription primaryKey,
                                                    final PropertyMapper mapper) {
        return new ReferencePropertyMapper(Schema.getDefaultSchema(), attribute, baseDN, primaryKey, mapper);
        return new ReferencePropertyMapper(Schema.getDefaultSchema(), attribute, baseDnTemplate, primaryKey, mapper);
    }
    /**
@@ -263,18 +265,21 @@
     *
     * @param attribute
     *         The DN valued LDAP attribute to be mapped.
     * @param baseDN
     *         The search base DN for performing reverse lookups.
     * @param baseDnTemplate
     *         The DN template which will be used as the search base when performing reverse lookups. The DN template
     *         may include template parameters and also parent RDNs using ".." notation. For example, the DN template
     *         "ou=groups,..,.." specifies that the search base DN should be computed by appending the RDN
     *         "ou=groups" to the grand-parent of the current resource's LDAP entry.
     * @param primaryKey
     *         The search primary key LDAP attribute to use for performing reverse lookups.
     * @param mapper
     *         An property mapper which will be used to map LDAP attributes in the referenced entry.
     * @return The property mapper.
     */
    public static ReferencePropertyMapper reference(final String attribute, final String baseDN,
    public static ReferencePropertyMapper reference(final String attribute, final String baseDnTemplate,
                                                    final String primaryKey, final PropertyMapper mapper) {
        return reference(AttributeDescription.valueOf(attribute),
                         DN.valueOf(baseDN),
                         baseDnTemplate,
                         AttributeDescription.valueOf(primaryKey),
                         mapper);
    }
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/RoutingContext.java
@@ -24,13 +24,23 @@
 * A {@link Context} which communicates the current Rest2Ldap routing state to downstream handlers.
 */
final class RoutingContext extends AbstractContext {
    static RoutingContext newCollectionRoutingContext(Context parent, DN collectionDn, Resource resource) {
        return new RoutingContext(parent, collectionDn, resource, true);
    }
    static RoutingContext newRoutingContext(Context parent, DN resourceDn, Resource resource) {
        return new RoutingContext(parent, resourceDn, resource, false);
    }
    private final DN dn;
    private final Resource resource;
    private final boolean isCollection;
    RoutingContext(final Context parent, final DN dn, final Resource resource) {
    private RoutingContext(Context parent, DN dn, Resource resource, boolean isCollection) {
        super(parent, "routing context");
        this.dn = dn;
        this.resource = resource;
        this.isCollection = isCollection;
    }
    DN getDn() {
@@ -40,4 +50,8 @@
    Resource getType() {
        return resource;
    }
    boolean isCollection() {
        return isCollection;
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceCollection.java
@@ -25,6 +25,7 @@
import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest;
import static org.forgerock.opendj.rest2ldap.Rest2Ldap.asResourceException;
import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.*;
import static org.forgerock.opendj.rest2ldap.RoutingContext.newRoutingContext;
import static org.forgerock.opendj.rest2ldap.Utils.newBadRequestException;
import static org.forgerock.util.promise.Promises.newResultPromise;
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceImpl.java
@@ -34,6 +34,8 @@
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;
import static org.forgerock.opendj.rest2ldap.RoutingContext.newRoutingContext;
import static org.forgerock.opendj.rest2ldap.Utils.connectionFrom;
import static org.forgerock.opendj.rest2ldap.Utils.newBadRequestException;
import static org.forgerock.opendj.rest2ldap.Utils.newNotSupportedException;
@@ -260,10 +262,15 @@
            return e.asPromise();
        }
        // Temporary routing context which will be used for encoding LDAP attributes. Note that the DN represents the
        // DN of the collection, not the resource being created. The DN of the resource can only be determined once
        // the LDAP attributes have been encoded.
        final RoutingContext parentDnAndType = newCollectionRoutingContext(context, baseDn, subType);
        // Now build the LDAP representation and add it.
        final Connection connection = connectionFrom(context);
        return subType.getPropertyMapper()
                      .create(context, subType, ROOT, request.getContent())
                      .create(parentDnAndType, subType, ROOT, request.getContent())
                      .thenAsync(new AsyncFunction<List<Attribute>, ResourceResponse, ResourceException>() {
                          @Override
                          public Promise<ResourceResponse, ResourceException> apply(final List<Attribute> attributes) {
@@ -284,9 +291,12 @@
                                          getLdapAttributesForKnownType(request.getFields(), subType);
                                  addRequest.addControl(PostReadRequestControl.newControl(false, ldapAttributes));
                              }
                              // Use a routing context which refers to the created entry when computing the response.
                              final RoutingContext dnAndType =
                                      newRoutingContext(context, addRequest.getName(), subType);
                              return connection.addAsync(addRequest)
                                               .thenCatchAsync(lazilyAddGlueEntry(connection, addRequest))
                                               .thenAsync(encodeUpdateResourceResponse(context, subType),
                                               .thenAsync(encodeUpdateResourceResponse(dnAndType, subType),
                                                          adaptLdapException(ResourceResponse.class));
                          }
                      });
@@ -346,7 +356,7 @@
                        return connection.applyChangeAsync(deleteRequest)
                                         .thenCatchAsync(deleteSubtreeWithoutUsingSubtreeDeleteControl(connection,
                                                                                                       deleteRequest))
                                         .thenAsync(encodeUpdateResourceResponse(context, dnAndType.getType()),
                                         .thenAsync(encodeUpdateResourceResponse(dnAndType, dnAndType.getType()),
                                                    adaptLdapException(ResourceResponse.class));
                    }
                });
@@ -433,7 +443,7 @@
                        final Resource subType = dnAndType.getType();
                        final PropertyMapper propertyMapper = subType.getPropertyMapper();
                        for (final PatchOperation operation : request.getPatchOperations()) {
                            promises.add(propertyMapper.patch(context, subType, ROOT, operation));
                            promises.add(propertyMapper.patch(dnAndType, subType, ROOT, operation));
                        }
                        return when(promises);
                    }
@@ -457,7 +467,7 @@
                        if (modifyRequest.getModifications().isEmpty()) {
                            // This patch is a no-op so just read the entry and check its version.
                            return connection.readEntryAsync(dnAndType.getDn(), attributes)
                                             .thenAsync(encodeEmptyPatchResourceResponse(context, subType, request),
                                             .thenAsync(encodeEmptyPatchResourceResponse(dnAndType, subType, request),
                                                        adaptLdapException(ResourceResponse.class));
                        } else {
                            // Add controls and perform the modify request.
@@ -469,7 +479,7 @@
                            }
                            addAssertionControl(modifyRequest, request.getRevision());
                            return connection.applyChangeAsync(modifyRequest)
                                             .thenAsync(encodeUpdateResourceResponse(context, subType),
                                             .thenAsync(encodeUpdateResourceResponse(dnAndType, subType),
                                                        adaptLdapException(ResourceResponse.class));
                        }
                    }
@@ -503,6 +513,9 @@
        if (queryFilter == null) {
            return new BadRequestException(ERR_QUERY_BY_ID_OR_EXPRESSION_NOT_SUPPORTED.get().toString()).asPromise();
        }
        // Temporary routing context which will be used for encoding the LDAP filter.
        final RoutingContext parentDnAndType = newCollectionRoutingContext(context, baseDn, resource);
        final PropertyMapper propertyMapper = resource.getPropertyMapper();
        final QueryFilterVisitor<Promise<Filter, ResourceException>, Void, JsonPointer> visitor =
                new QueryFilterVisitor<Promise<Filter, ResourceException>, Void, JsonPointer>() {
@@ -549,14 +562,14 @@
                    public Promise<Filter, ResourceException> visitContainsFilter(
                            final Void unused, final JsonPointer field, final Object valueAssertion) {
                        return propertyMapper.getLdapFilter(
                                context, resource, ROOT, field, CONTAINS, null, valueAssertion);
                                parentDnAndType, resource, ROOT, field, CONTAINS, null, valueAssertion);
                    }
                    @Override
                    public Promise<Filter, ResourceException> visitEqualsFilter(
                            final Void unused, final JsonPointer field, final Object valueAssertion) {
                        return propertyMapper.getLdapFilter(
                                context, resource, ROOT, field, EQUAL_TO, null, valueAssertion);
                                parentDnAndType, resource, ROOT, field, EQUAL_TO, null, valueAssertion);
                    }
                    @Override
@@ -565,35 +578,35 @@
                                                                                       final String operator,
                                                                                       final Object valueAssertion) {
                        return propertyMapper.getLdapFilter(
                                context, resource, ROOT, field, EXTENDED, operator, valueAssertion);
                                parentDnAndType, resource, ROOT, field, EXTENDED, operator, valueAssertion);
                    }
                    @Override
                    public Promise<Filter, ResourceException> visitGreaterThanFilter(
                            final Void unused, final JsonPointer field, final Object valueAssertion) {
                        return propertyMapper.getLdapFilter(
                                context, resource, ROOT, field, GREATER_THAN, null, valueAssertion);
                                parentDnAndType, resource, ROOT, field, GREATER_THAN, null, valueAssertion);
                    }
                    @Override
                    public Promise<Filter, ResourceException> visitGreaterThanOrEqualToFilter(
                            final Void unused, final JsonPointer field, final Object valueAssertion) {
                        return propertyMapper.getLdapFilter(
                                context, resource, ROOT, field, GREATER_THAN_OR_EQUAL_TO, null, valueAssertion);
                                parentDnAndType, resource, ROOT, field, GREATER_THAN_OR_EQUAL_TO, null, valueAssertion);
                    }
                    @Override
                    public Promise<Filter, ResourceException> visitLessThanFilter(
                            final Void unused, final JsonPointer field, final Object valueAssertion) {
                        return propertyMapper.getLdapFilter(
                                context, resource, ROOT, field, LESS_THAN, null, valueAssertion);
                                parentDnAndType, resource, ROOT, field, LESS_THAN, null, valueAssertion);
                    }
                    @Override
                    public Promise<Filter, ResourceException> visitLessThanOrEqualToFilter(
                            final Void unused, final JsonPointer field, final Object valueAssertion) {
                        return propertyMapper.getLdapFilter(
                                context, resource, ROOT, field, LESS_THAN_OR_EQUAL_TO, null, valueAssertion);
                                parentDnAndType, resource, ROOT, field, LESS_THAN_OR_EQUAL_TO, null, valueAssertion);
                    }
                    @Override
@@ -649,14 +662,15 @@
                    @Override
                    public Promise<Filter, ResourceException> visitPresentFilter(
                            final Void unused, final JsonPointer field) {
                        return propertyMapper.getLdapFilter(context, resource, ROOT, field, PRESENT, null, null);
                        return propertyMapper.getLdapFilter(
                                parentDnAndType, resource, ROOT, field, PRESENT, null, null);
                    }
                    @Override
                    public Promise<Filter, ResourceException> visitStartsWithFilter(
                            final Void unused, final JsonPointer field, final Object valueAssertion) {
                        return propertyMapper.getLdapFilter(
                                context, resource, ROOT, field, STARTS_WITH, null, valueAssertion);
                                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.
@@ -741,8 +755,9 @@
                        final String id = namingStrategy.decodeResourceId(entry);
                        final String revision = getRevisionFromEntry(entry);
                        final Resource subType = resource.resolveSubTypeFromObjectClasses(entry);
                        final RoutingContext dnAndType = newRoutingContext(context, entry.getName(), subType);
                        final PropertyMapper propertyMapper = subType.getPropertyMapper();
                        propertyMapper.read(context, subType, ROOT, entry)
                        propertyMapper.read(dnAndType, subType, ROOT, entry)
                                      .thenOnResult(new ResultHandler<JsonValue>() {
                                          @Override
                                          public void handleResult(final JsonValue result) {
@@ -845,7 +860,8 @@
                             @Override
                             public Promise<ResourceResponse, ResourceException> apply(SearchResultEntry entry) {
                                 final Resource subType = resource.resolveSubTypeFromObjectClasses(entry);
                                 return encodeResourceResponse(context, subType, entry);
                                 final RoutingContext dnAndType = newRoutingContext(context, entry.getName(), subType);
                                 return encodeResourceResponse(dnAndType, subType, entry);
                             }
                         });
    }
@@ -854,7 +870,7 @@
            final Context context, final String resourceId, final UpdateRequest request) {
        final Connection connection = connectionFrom(context);
        final AtomicReference<Entry> entryHolder = new AtomicReference<>();
        final AtomicReference<Resource> subTypeHolder = new AtomicReference<>();
        final AtomicReference<RoutingContext> dnAndTypeHolder = new AtomicReference<>();
        return connection
                .searchSingleEntryAsync(searchRequestForUnknownType(resourceId, Collections.<JsonPointer>emptyList()))
                .thenCatchAsync(adaptLdapException(SearchResultEntry.class))
@@ -869,18 +885,20 @@
                        // Determine the type of resource and set of changes that need to be performed.
                        final Resource subType = resource.resolveSubTypeFromObjectClasses(entry);
                        subTypeHolder.set(subType);
                        final RoutingContext dnAndType = newRoutingContext(context, entry.getName(), subType);
                        dnAndTypeHolder.set(dnAndType);
                        final PropertyMapper propertyMapper = subType.getPropertyMapper();
                        return propertyMapper.update(context, subType , ROOT, entry, request.getContent());
                        return propertyMapper.update(dnAndType, subType , ROOT, entry, request.getContent());
                    }
                }).thenAsync(new AsyncFunction<List<Modification>, ResourceResponse, ResourceException>() {
                    @Override
                    public Promise<ResourceResponse, ResourceException> apply(List<Modification> modifications)
                            throws ResourceException {
                        final Resource subType = subTypeHolder.get();
                        final RoutingContext dnAndType = dnAndTypeHolder.get();
                        final Resource subType = dnAndType.getType();
                        if (modifications.isEmpty()) {
                            // No changes to be performed so just return the entry that we read.
                            return encodeResourceResponse(context, subType, entryHolder.get());
                            return encodeResourceResponse(dnAndType, subType, entryHolder.get());
                        }
                        // Perform the modify operation.
                        final ModifyRequest modifyRequest = newModifyRequest(entryHolder.get().getName());
@@ -894,7 +912,7 @@
                        addAssertionControl(modifyRequest, request.getRevision());
                        modifyRequest.getModifications().addAll(modifications);
                        return connection.applyChangeAsync(modifyRequest)
                                         .thenAsync(encodeUpdateResourceResponse(context, subType),
                                         .thenAsync(encodeUpdateResourceResponse(dnAndType, subType),
                                                    adaptLdapException(ResourceResponse.class));
                    }
                });
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceSingleton.java
@@ -23,6 +23,7 @@
import static org.forgerock.opendj.ldap.SearchScope.BASE_OBJECT;
import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest;
import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_UNSUPPORTED_REQUEST_AGAINST_SINGLETON;
import static org.forgerock.opendj.rest2ldap.RoutingContext.newRoutingContext;
import static org.forgerock.util.promise.Promises.newResultPromise;
import org.forgerock.json.resource.ActionRequest;
opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/DnTemplateTest.java
@@ -18,6 +18,7 @@
import static java.util.Collections.singletonMap;
import static org.assertj.core.api.Assertions.assertThat;
import static org.forgerock.opendj.rest2ldap.Rest2Ldap.rest2Ldap;
import static org.forgerock.opendj.rest2ldap.RoutingContext.newRoutingContext;
import static org.forgerock.util.Options.defaultOptions;
import org.forgerock.http.routing.UriRouterContext;