/* * The contents of this file are subject to the terms of the Common Development and * Distribution License (the License). You may not use this file except in compliance with the * License. * * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the * specific language governing permission and limitations under the License. * * When distributing Covered Software, include this CDDL Header Notice in each file and include * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions Copyright [year] [name of copyright owner]". * * Copyright 2012-2013 ForgeRock AS. */ package org.forgerock.opendj.rest2ldap; import static org.forgerock.opendj.ldap.Filter.alwaysFalse; import static org.forgerock.opendj.ldap.Filter.alwaysTrue; import static org.forgerock.opendj.ldap.requests.Requests.newAddRequest; import static org.forgerock.opendj.ldap.requests.Requests.newDeleteRequest; import static org.forgerock.opendj.ldap.requests.Requests.newModifyRequest; import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest; import static org.forgerock.opendj.rest2ldap.ReadOnUpdatePolicy.CONTROLS; import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException; import static org.forgerock.opendj.rest2ldap.Utils.accumulate; import static org.forgerock.opendj.rest2ldap.Utils.i18n; import static org.forgerock.opendj.rest2ldap.Utils.toFilter; import static org.forgerock.opendj.rest2ldap.Utils.transform; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import org.forgerock.json.fluent.JsonPointer; import org.forgerock.json.fluent.JsonValue; import org.forgerock.json.resource.ActionRequest; import org.forgerock.json.resource.CollectionResourceProvider; import org.forgerock.json.resource.CreateRequest; import org.forgerock.json.resource.DeleteRequest; import org.forgerock.json.resource.InternalServerErrorException; import org.forgerock.json.resource.NotSupportedException; import org.forgerock.json.resource.PatchRequest; import org.forgerock.json.resource.PreconditionFailedException; import org.forgerock.json.resource.QueryFilter; import org.forgerock.json.resource.QueryFilterVisitor; import org.forgerock.json.resource.QueryRequest; import org.forgerock.json.resource.QueryResult; import org.forgerock.json.resource.QueryResultHandler; import org.forgerock.json.resource.ReadRequest; import org.forgerock.json.resource.Resource; import org.forgerock.json.resource.ResourceException; import org.forgerock.json.resource.ResultHandler; import org.forgerock.json.resource.ServerContext; import org.forgerock.json.resource.UncategorizedException; import org.forgerock.json.resource.UpdateRequest; import org.forgerock.opendj.ldap.Attribute; import org.forgerock.opendj.ldap.AttributeDescription; import org.forgerock.opendj.ldap.DN; import org.forgerock.opendj.ldap.DecodeException; import org.forgerock.opendj.ldap.Entry; import org.forgerock.opendj.ldap.ErrorResultException; import org.forgerock.opendj.ldap.Filter; import org.forgerock.opendj.ldap.Function; import org.forgerock.opendj.ldap.Modification; import org.forgerock.opendj.ldap.ModificationType; import org.forgerock.opendj.ldap.SearchResultHandler; import org.forgerock.opendj.ldap.SearchScope; import org.forgerock.opendj.ldap.controls.AssertionRequestControl; import org.forgerock.opendj.ldap.controls.PermissiveModifyRequestControl; import org.forgerock.opendj.ldap.controls.PostReadRequestControl; import org.forgerock.opendj.ldap.controls.PostReadResponseControl; import org.forgerock.opendj.ldap.controls.PreReadRequestControl; import org.forgerock.opendj.ldap.controls.PreReadResponseControl; import org.forgerock.opendj.ldap.controls.SubtreeDeleteRequestControl; import org.forgerock.opendj.ldap.requests.AddRequest; import org.forgerock.opendj.ldap.requests.ModifyRequest; import org.forgerock.opendj.ldap.requests.SearchRequest; import org.forgerock.opendj.ldap.responses.Result; import org.forgerock.opendj.ldap.responses.SearchResultEntry; import org.forgerock.opendj.ldap.responses.SearchResultReference; import org.forgerock.opendj.ldif.ChangeRecord; /** * A {@code CollectionResourceProvider} implementation which maps a JSON * resource collection to LDAP entries beneath a base DN. */ final class LDAPCollectionResourceProvider implements CollectionResourceProvider { // Dummy exception used for signalling search success. private static final ResourceException SUCCESS = new UncategorizedException(0, null, null); private final List additionalLDAPAttributes; private final AttributeMapper attributeMapper; private final DN baseDN; // TODO: support template variables. private final Config config; private final AttributeDescription etagAttribute; private final NameStrategy nameStrategy; LDAPCollectionResourceProvider(final DN baseDN, final AttributeMapper mapper, final NameStrategy nameStrategy, final AttributeDescription etagAttribute, final Config config, final List additionalLDAPAttributes) { this.baseDN = baseDN; this.attributeMapper = mapper; this.config = config; this.nameStrategy = nameStrategy; this.etagAttribute = etagAttribute; this.additionalLDAPAttributes = additionalLDAPAttributes; } @Override public void actionCollection(final ServerContext context, final ActionRequest request, final ResultHandler handler) { handler.handleError(new NotSupportedException("Not yet implemented")); } @Override public void actionInstance(final ServerContext context, final String resourceId, final ActionRequest request, final ResultHandler handler) { handler.handleError(new NotSupportedException("Not yet implemented")); } @Override public void createInstance(final ServerContext context, final CreateRequest request, final ResultHandler handler) { final Context c = wrap(context); final ResultHandler h = wrap(c, handler); // Get the connection, then determine entry content, then perform add. c.run(h, new Runnable() { @Override public void run() { // Calculate entry content. attributeMapper.toLDAP(c, new JsonPointer(), null, request.getContent(), new ResultHandler>() { @Override public void handleError(final ResourceException error) { h.handleError(error); } @Override public void handleResult(final List result) { // Perform add operation. final AddRequest addRequest = newAddRequest(DN.rootDN()); for (final Attribute attribute : additionalLDAPAttributes) { addRequest.addAttribute(attribute); } for (final Modification modification : result) { if (modification.getModificationType() == ModificationType.ADD || modification.getModificationType() == ModificationType.REPLACE) { addRequest.addAttribute(modification.getAttribute()); } else { // Attribute mappers must return add/replace updates. h.handleError(new InternalServerErrorException( i18n("Attribute mapper returned a modification which " + "does not add an attribute"))); return; } } try { nameStrategy.setResourceId(c, getBaseDN(c), request .getNewResourceId(), addRequest); } catch (final ResourceException e) { h.handleError(e); return; } if (config.readOnUpdatePolicy() == CONTROLS) { final String[] attributes = getLDAPAttributes(c, request.getFields()); addRequest.addControl(PostReadRequestControl.newControl(false, attributes)); } c.getConnection().applyChangeAsync(addRequest, null, postUpdateHandler(c, h)); } }); } }); } @Override public void deleteInstance(final ServerContext context, final String resourceId, final DeleteRequest request, final ResultHandler handler) { final Context c = wrap(context); final ResultHandler h = wrap(c, handler); // Get connection then perform the search. c.run(h, new Runnable() { @Override public void run() { // Find the entry and then delete it. final String ldapAttribute = (etagAttribute != null && request.getRevision() != null) ? etagAttribute .toString() : "1.1"; final SearchRequest searchRequest = nameStrategy.createSearchRequest(c, getBaseDN(c), resourceId).addAttribute( ldapAttribute); c.getConnection().searchSingleEntryAsync(searchRequest, new org.forgerock.opendj.ldap.ResultHandler() { @Override public void handleErrorResult(final ErrorResultException error) { h.handleError(asResourceException(error)); } @Override public void handleResult(final SearchResultEntry entry) { try { // Fail-fast if there is a version mismatch. ensureMVCCVersionMatches(entry, request.getRevision()); // Perform delete operation. final ChangeRecord deleteRequest = newDeleteRequest(entry.getName()); if (config.readOnUpdatePolicy() == CONTROLS) { final String[] attributes = getLDAPAttributes(c, request.getFields()); deleteRequest.addControl(PreReadRequestControl.newControl( false, attributes)); } if (config.useSubtreeDelete()) { deleteRequest.addControl(SubtreeDeleteRequestControl .newControl(true)); } addAssertionControl(deleteRequest, request.getRevision()); c.getConnection().applyChangeAsync(deleteRequest, null, postUpdateHandler(c, h)); } catch (final Exception e) { h.handleError(asResourceException(e)); } } }); } }); } @Override public void patchInstance(final ServerContext context, final String resourceId, final PatchRequest request, final ResultHandler handler) { handler.handleError(new NotSupportedException("Not yet implemented")); } @Override public void queryCollection(final ServerContext context, final QueryRequest request, final QueryResultHandler handler) { final Context c = wrap(context); final QueryResultHandler h = wrap(c, handler); /* * Get the connection, then calculate the search filter, then perform * the search. */ c.run(h, new Runnable() { @Override public void run() { // Calculate the filter (this may require the connection). getLDAPFilter(c, request.getQueryFilter(), new ResultHandler() { @Override public void handleError(final ResourceException error) { h.handleError(error); } @Override public void handleResult(final Filter ldapFilter) { /* * Avoid performing a search if the filter could not be * mapped or if it will never match. */ if (ldapFilter == null || ldapFilter == alwaysFalse()) { h.handleResult(new QueryResult()); } else { // Perform the search. final String[] attributes = getLDAPAttributes(c, request.getFields()); final SearchRequest request = newSearchRequest(getBaseDN(c), SearchScope.SINGLE_LEVEL, ldapFilter == Filter.alwaysTrue() ? Filter .objectClassPresent() : ldapFilter, attributes); c.getConnection().searchAsync(request, null, new SearchResultHandler() { private final AtomicInteger pendingResourceCount = new AtomicInteger(); private final AtomicReference pendingResult = new AtomicReference(); private final AtomicBoolean resultSent = new AtomicBoolean(); @Override public boolean handleEntry(final SearchResultEntry entry) { /* * Search result entries will be returned * before the search result/error so the * only reason pendingResult will be * non-null is if a mapping error has * occurred. */ if (pendingResult.get() != null) { return false; } final String id = nameStrategy.getResourceId(c, entry); final String revision = getRevisionFromEntry(entry); final ResultHandler mapHandler = new ResultHandler() { @Override public void handleError(final ResourceException e) { pendingResult.compareAndSet(null, e); pendingResourceCount.decrementAndGet(); completeIfNecessary(); } @Override public void handleResult(final JsonValue result) { final Resource resource = new Resource(id, revision, result); h.handleResource(resource); pendingResourceCount.decrementAndGet(); completeIfNecessary(); } }; pendingResourceCount.incrementAndGet(); attributeMapper.toJSON(c, new JsonPointer(), entry, mapHandler); return true; } @Override public void handleErrorResult(final ErrorResultException error) { pendingResult.compareAndSet(null, asResourceException(error)); completeIfNecessary(); } @Override public boolean handleReference(final SearchResultReference reference) { // TODO: should this be classed as an error since rest2ldap // assumes entries are all colocated? return true; } @Override public void handleResult(final Result result) { pendingResult.compareAndSet(null, SUCCESS); completeIfNecessary(); } /* * Close out the query result set if there are * no more pending resources and the LDAP result * has been received. */ private void completeIfNecessary() { if (pendingResourceCount.get() == 0) { final ResourceException result = pendingResult.get(); if (result != null && resultSent.compareAndSet(false, true)) { if (result == SUCCESS) { h.handleResult(new QueryResult()); } else { h.handleError(result); } } } } }); } } }); } }); } @Override public void readInstance(final ServerContext context, final String resourceId, final ReadRequest request, final ResultHandler handler) { final Context c = wrap(context); final ResultHandler h = wrap(c, handler); // Get connection then perform the search. c.run(h, new Runnable() { @Override public void run() { // Do the search. final String[] attributes = getLDAPAttributes(c, request.getFields()); final SearchRequest request = nameStrategy.createSearchRequest(c, getBaseDN(c), resourceId).addAttribute( attributes); c.getConnection().searchSingleEntryAsync(request, new org.forgerock.opendj.ldap.ResultHandler() { @Override public void handleErrorResult(final ErrorResultException error) { h.handleError(asResourceException(error)); } @Override public void handleResult(final SearchResultEntry entry) { adaptEntry(c, entry, h); } }); } }); } @Override public void updateInstance(final ServerContext context, final String resourceId, final UpdateRequest request, final ResultHandler handler) { /* * Update operations are a bit awkward because there is not direct * mapping to LDAP. We need to convert the update request into an LDAP * modify operation which means reading the current LDAP entry, * generating the new entry content, then comparing the two in order to * obtain a set of changes. We also need to handle read-only fields * correctly: if a read-only field is included with the new resource * then it must match exactly the value of the existing field. */ final Context c = wrap(context); final ResultHandler h = wrap(c, handler); // Get connection then perform the search. c.run(h, new Runnable() { @Override public void run() { // First of all read the existing entry. final String[] attributes = getLDAPAttributes(c, Collections. emptyList()); final SearchRequest searchRequest = nameStrategy.createSearchRequest(c, getBaseDN(c), resourceId).addAttribute( attributes); c.getConnection().searchSingleEntryAsync(searchRequest, new org.forgerock.opendj.ldap.ResultHandler() { @Override public void handleErrorResult(final ErrorResultException error) { h.handleError(asResourceException(error)); } @Override public void handleResult(final SearchResultEntry entry) { try { // Fail-fast if there is a version mismatch. ensureMVCCVersionMatches(entry, request.getRevision()); // Create the modify request. final ModifyRequest modifyRequest = newModifyRequest(entry.getName()); if (config.readOnUpdatePolicy() == CONTROLS) { final String[] attributes = getLDAPAttributes(c, request.getFields()); modifyRequest.addControl(PostReadRequestControl.newControl( false, attributes)); } if (config.usePermissiveModify()) { modifyRequest.addControl(PermissiveModifyRequestControl .newControl(true)); } addAssertionControl(modifyRequest, request.getRevision()); /* * Determine the set of changes that need to * be performed. */ attributeMapper.toLDAP(c, new JsonPointer(), entry, request .getNewContent(), new ResultHandler>() { @Override public void handleError( final ResourceException error) { h.handleError(error); } @Override public void handleResult( final List result) { // Perform the modify operation. modifyRequest.getModifications().addAll(result); c.getConnection().applyChangeAsync( modifyRequest, null, postUpdateHandler(c, h)); } }); } catch (final Exception e) { h.handleError(asResourceException(e)); } } }); } }); } private void adaptEntry(final Context c, final Entry entry, final ResultHandler handler) { final String actualResourceId = nameStrategy.getResourceId(c, entry); final String revision = getRevisionFromEntry(entry); attributeMapper.toJSON(c, new JsonPointer(), entry, transform( new Function() { @Override public Resource apply(final JsonValue value, final Void p) { return new Resource(actualResourceId, revision, new JsonValue(value)); } }, handler)); } private void addAssertionControl(final ChangeRecord request, final String expectedRevision) throws ResourceException { if (expectedRevision != null) { ensureMVCCSupported(); request.addControl(AssertionRequestControl.newControl(true, Filter.equality( etagAttribute.toString(), expectedRevision))); } } private void ensureMVCCSupported() throws NotSupportedException { if (etagAttribute == null) { throw new NotSupportedException( i18n("Multi-version concurrency control is not supported by this resource")); } } private void ensureMVCCVersionMatches(final Entry entry, final String expectedRevision) throws ResourceException { if (expectedRevision != null) { ensureMVCCSupported(); final String actualRevision = entry.parseAttribute(etagAttribute).asString(); if (actualRevision == null) { throw new PreconditionFailedException(i18n( "The resource could not be accessed because it did not contain any " + "version information, when the version '%s' was expected", expectedRevision)); } else if (!expectedRevision.equals(actualRevision)) { throw new PreconditionFailedException(i18n( "The resource could not be accessed because the expected version '%s' " + "does not match the current version '%s'", expectedRevision, actualRevision)); } } } private DN getBaseDN(final Context context) { return baseDN; } /** * Determines the set of LDAP attributes to request in an LDAP read (search, * post-read), based on the provided list of JSON pointers. * * @param requestedAttributes * The list of resource attributes to be read. * @return The set of LDAP attributes associated with the resource * attributes. */ private String[] getLDAPAttributes(final Context c, final Collection requestedAttributes) { // Get all the LDAP attributes required by the attribute mappers. final Set requestedLDAPAttributes; if (requestedAttributes.isEmpty()) { // Full read. requestedLDAPAttributes = new LinkedHashSet(); attributeMapper.getLDAPAttributes(c, new JsonPointer(), new JsonPointer(), requestedLDAPAttributes); } else { // Partial read. requestedLDAPAttributes = new LinkedHashSet(requestedAttributes.size()); for (final JsonPointer requestedAttribute : requestedAttributes) { attributeMapper.getLDAPAttributes(c, new JsonPointer(), requestedAttribute, requestedLDAPAttributes); } } // Get the LDAP attributes required by the Etag and name stategies. nameStrategy.getLDAPAttributes(c, requestedLDAPAttributes); if (etagAttribute != null) { requestedLDAPAttributes.add(etagAttribute.toString()); } return requestedLDAPAttributes.toArray(new String[requestedLDAPAttributes.size()]); } private void getLDAPFilter(final Context c, final QueryFilter queryFilter, final ResultHandler h) { final QueryFilterVisitor> visitor = new QueryFilterVisitor>() { @Override public Void visitAndFilter(final ResultHandler p, final List subFilters) { final ResultHandler handler = accumulate(subFilters.size(), transform( new Function, Filter, Void>() { @Override public Filter apply(final List value, final Void p) { // Check for unmapped filter components and optimize. final Iterator 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(); case 1: return value.get(0); default: return Filter.and(value); } } }, p)); for (final QueryFilter subFilter : subFilters) { subFilter.accept(this, handler); } return null; } @Override public Void visitBooleanLiteralFilter(final ResultHandler p, final boolean value) { p.handleResult(toFilter(value)); return null; } @Override public Void visitContainsFilter(final ResultHandler p, final JsonPointer field, final Object valueAssertion) { attributeMapper.getLDAPFilter(c, new JsonPointer(), field, FilterType.CONTAINS, null, valueAssertion, p); return null; } @Override public Void visitEqualsFilter(final ResultHandler p, final JsonPointer field, final Object valueAssertion) { attributeMapper.getLDAPFilter(c, new JsonPointer(), field, FilterType.EQUAL_TO, null, valueAssertion, p); return null; } @Override public Void visitExtendedMatchFilter(final ResultHandler p, final JsonPointer field, final String operator, final Object valueAssertion) { attributeMapper.getLDAPFilter(c, new JsonPointer(), field, FilterType.EXTENDED, operator, valueAssertion, p); return null; } @Override public Void visitGreaterThanFilter(final ResultHandler p, final JsonPointer field, final Object valueAssertion) { attributeMapper.getLDAPFilter(c, new JsonPointer(), field, FilterType.GREATER_THAN, null, valueAssertion, p); return null; } @Override public Void visitGreaterThanOrEqualToFilter(final ResultHandler p, final JsonPointer field, final Object valueAssertion) { attributeMapper.getLDAPFilter(c, new JsonPointer(), field, FilterType.GREATER_THAN_OR_EQUAL_TO, null, valueAssertion, p); return null; } @Override public Void visitLessThanFilter(final ResultHandler p, final JsonPointer field, final Object valueAssertion) { attributeMapper.getLDAPFilter(c, new JsonPointer(), field, FilterType.LESS_THAN, null, valueAssertion, p); return null; } @Override public Void visitLessThanOrEqualToFilter(final ResultHandler p, final JsonPointer field, final Object valueAssertion) { attributeMapper.getLDAPFilter(c, new JsonPointer(), field, FilterType.LESS_THAN_OR_EQUAL_TO, null, valueAssertion, p); return null; } @Override public Void visitNotFilter(final ResultHandler p, final QueryFilter subFilter) { subFilter.accept(this, transform(new Function() { @Override public Filter apply(final Filter value, final Void p) { if (value == null || value == alwaysFalse()) { return alwaysTrue(); } else if (value == alwaysTrue()) { return alwaysFalse(); } else { return Filter.not(value); } } }, p)); return null; } @Override public Void visitOrFilter(final ResultHandler p, final List subFilters) { final ResultHandler handler = accumulate(subFilters.size(), transform( new Function, Filter, Void>() { @Override public Filter apply(final List value, final Void p) { // Check for unmapped filter components and optimize. final Iterator i = value.iterator(); while (i.hasNext()) { final Filter f = i.next(); if (f == alwaysFalse()) { i.remove(); } else if (f == alwaysTrue()) { return alwaysTrue(); } } switch (value.size()) { case 0: return alwaysFalse(); case 1: return value.get(0); default: return Filter.or(value); } } }, p)); for (final QueryFilter subFilter : subFilters) { subFilter.accept(this, handler); } return null; } @Override public Void visitPresentFilter(final ResultHandler p, final JsonPointer field) { attributeMapper.getLDAPFilter(c, new JsonPointer(), field, FilterType.PRESENT, null, null, p); return null; } @Override public Void visitStartsWithFilter(final ResultHandler p, final JsonPointer field, final Object valueAssertion) { attributeMapper.getLDAPFilter(c, new JsonPointer(), field, FilterType.STARTS_WITH, null, valueAssertion, p); return null; } }; /* * Note that the returned LDAP filter may be null if it could not be * mapped by any attribute mappers. */ queryFilter.accept(visitor, h); } private String getRevisionFromEntry(final Entry entry) { return etagAttribute != null ? entry.parseAttribute(etagAttribute).asString() : null; } private org.forgerock.opendj.ldap.ResultHandler postUpdateHandler(final Context c, final ResultHandler handler) { // The handler which will be invoked for the LDAP add result. return new org.forgerock.opendj.ldap.ResultHandler() { @Override public void handleErrorResult(final ErrorResultException error) { handler.handleError(asResourceException(error)); } @Override public void handleResult(final Result result) { // FIXME: handle USE_SEARCH policy. Entry entry; try { final PostReadResponseControl postReadControl = result.getControl(PostReadResponseControl.DECODER, config .decodeOptions()); if (postReadControl != null) { entry = postReadControl.getEntry(); } else { final PreReadResponseControl preReadControl = result.getControl(PreReadResponseControl.DECODER, config .decodeOptions()); if (preReadControl != null) { entry = preReadControl.getEntry(); } else { entry = null; } } } catch (final DecodeException e) { // FIXME: log something? entry = null; } if (entry != null) { adaptEntry(c, entry, handler); } else { final Resource resource = new Resource(null, null, new JsonValue(Collections.emptyMap())); handler.handleResult(resource); } } }; } private QueryResultHandler wrap(final Context c, final QueryResultHandler handler) { return new QueryResultHandler() { @Override public void handleError(final ResourceException error) { c.close(); handler.handleError(error); } @Override public boolean handleResource(final Resource resource) { return handler.handleResource(resource); } @Override public void handleResult(final QueryResult result) { c.close(); handler.handleResult(result); } }; } private ResultHandler wrap(final Context c, final ResultHandler handler) { return new ResultHandler() { @Override public void handleError(final ResourceException error) { c.close(); handler.handleError(error); } @Override public void handleResult(final V result) { c.close(); handler.handleResult(result); } }; } private Context wrap(final ServerContext context) { return new Context(config, context); } }