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;