/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt * or http://forgerock.org/license/CDDLv1.0.html. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at legal-notices/CDDLv1_0.txt. * If applicable, add the following below this CDDL HEADER, with the * fields enclosed by brackets "[]" replaced with your own identifying * information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * Copyright 2013 ForgeRock AS. */ package org.forgerock.opendj.ldap; import static org.forgerock.opendj.ldap.Attributes.singletonAttribute; import static org.forgerock.opendj.ldap.Entries.modifyEntry; import static org.forgerock.opendj.ldap.ErrorResultException.newErrorResult; import static org.forgerock.opendj.ldap.responses.Responses.newBindResult; import static org.forgerock.opendj.ldap.responses.Responses.newCompareResult; import static org.forgerock.opendj.ldap.responses.Responses.newResult; import static org.forgerock.opendj.ldap.responses.Responses.newSearchResultEntry; import java.io.IOException; import java.util.NavigableMap; import java.util.concurrent.ConcurrentSkipListMap; import org.forgerock.i18n.LocalizedIllegalArgumentException; import org.forgerock.opendj.ldap.controls.AssertionRequestControl; 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.BindRequest; import org.forgerock.opendj.ldap.requests.CompareRequest; import org.forgerock.opendj.ldap.requests.DeleteRequest; import org.forgerock.opendj.ldap.requests.ExtendedRequest; import org.forgerock.opendj.ldap.requests.GenericBindRequest; import org.forgerock.opendj.ldap.requests.ModifyDNRequest; import org.forgerock.opendj.ldap.requests.ModifyRequest; import org.forgerock.opendj.ldap.requests.Request; import org.forgerock.opendj.ldap.requests.SearchRequest; import org.forgerock.opendj.ldap.requests.SimpleBindRequest; import org.forgerock.opendj.ldap.responses.BindResult; import org.forgerock.opendj.ldap.responses.CompareResult; import org.forgerock.opendj.ldap.responses.ExtendedResult; import org.forgerock.opendj.ldap.responses.Result; import org.forgerock.opendj.ldap.schema.Schema; import org.forgerock.opendj.ldif.EntryReader; /** * A simple in memory back-end which can be used for testing. It is not intended * for production use due to various limitations. The back-end implementations * supports the following: * * It does not support the following: * * This class can be used in conjunction with the factories defined in * {@link Connections} to create simpler servers as well as mock LDAP * connections. For example, to create a mock LDAP connection factory: * *
 * MemoryBackend backend = new MemoryBackend();
 * Connection connection = newInternalConnectionFactory(newServerConnectionFactory(backend), null)
 *         .getConnection();
 * 
*/ public final class MemoryBackend implements RequestHandler { private final DecodeOptions decodeOptions; private final ConcurrentSkipListMap entries = new ConcurrentSkipListMap(); private final Object writeLock = new Object(); private final Schema schema; /** * Creates a new empty memory backend which will use the default schema. */ public MemoryBackend() { this(Schema.getDefaultSchema()); } /** * Creates a new memory backend which will use the default schema, and will * contain the entries read from the provided entry reader. * * @param reader * The entry reader. * @throws IOException * If an unexpected IO error occurred while reading the entries. */ public MemoryBackend(final EntryReader reader) throws IOException { this(Schema.getDefaultSchema(), reader); } /** * Creates a new empty memory backend which will use the provided schema. * * @param schema * The schema to use for decoding filters, etc. */ public MemoryBackend(final Schema schema) { this.schema = schema; this.decodeOptions = new DecodeOptions().setSchema(schema); } /** * Creates a new memory backend which will use the provided schema, and will * contain the entries read from the provided entry reader. * * @param schema * The schema to use for decoding filters, etc. * @param reader * The entry reader. * @throws IOException * If an unexpected IO error occurred while reading the entries. */ public MemoryBackend(final Schema schema, final EntryReader reader) throws IOException { this.schema = schema; this.decodeOptions = new DecodeOptions().setSchema(schema); if (reader != null) { try { while (reader.hasNext()) { final Entry entry = reader.readEntry(); final DN dn = entry.getName(); if (entries.containsKey(dn)) { throw new ErrorResultIOException(newErrorResult( ResultCode.ENTRY_ALREADY_EXISTS, "Attempted to add the entry '" + dn.toString() + "' multiple times")); } else { entries.put(dn, entry); } } } finally { reader.close(); } } } @Override public void handleAdd(final RequestContext requestContext, final AddRequest request, final IntermediateResponseHandler intermediateResponseHandler, final ResultHandler resultHandler) { try { synchronized (writeLock) { final DN dn = request.getName(); final DN parent = dn.parent(); if (entries.containsKey(dn)) { throw newErrorResult(ResultCode.ENTRY_ALREADY_EXISTS, "The entry '" + dn.toString() + "' already exists"); } else if (!entries.containsKey(parent)) { noSuchObject(parent); } else { entries.put(dn, request); } } resultHandler.handleResult(getResult(request, null, request)); } catch (final ErrorResultException e) { resultHandler.handleErrorResult(e); } } @Override public void handleBind(final RequestContext requestContext, final int version, final BindRequest request, final IntermediateResponseHandler intermediateResponseHandler, final ResultHandler resultHandler) { try { final Entry entry; synchronized (writeLock) { final DN username = DN.valueOf(request.getName(), schema); final byte[] password; if (request instanceof SimpleBindRequest) { password = ((SimpleBindRequest) request).getPassword(); } else if (request instanceof GenericBindRequest && request.getAuthenticationType() == BindRequest.AUTHENTICATION_TYPE_SIMPLE) { password = ((GenericBindRequest) request).getAuthenticationValue(); } else { throw newErrorResult(ResultCode.PROTOCOL_ERROR, "non-SIMPLE authentication not supported: " + request.getAuthenticationType()); } entry = getRequiredEntry(null, username); if (!entry.containsAttribute("userPassword", password)) { throw newErrorResult(ResultCode.INVALID_CREDENTIALS, "Wrong password"); } } resultHandler.handleResult(getBindResult(request, entry, entry)); } catch (final LocalizedIllegalArgumentException e) { resultHandler.handleErrorResult(newErrorResult(ResultCode.PROTOCOL_ERROR, e)); } catch (final EntryNotFoundException e) { /* * Usually you would not include a diagnostic message, but we'll add * one here because the memory back-end is not intended for * production use. */ resultHandler.handleErrorResult(newErrorResult(ResultCode.INVALID_CREDENTIALS, "Unknown user")); } catch (final ErrorResultException e) { resultHandler.handleErrorResult(e); } } @Override public void handleCompare(final RequestContext requestContext, final CompareRequest request, final IntermediateResponseHandler intermediateResponseHandler, final ResultHandler resultHandler) { try { final Entry entry; final Attribute assertion; synchronized (writeLock) { final DN dn = request.getName(); entry = getRequiredEntry(request, dn); assertion = singletonAttribute(request.getAttributeDescription(), request .getAssertionValue()); } resultHandler.handleResult(getCompareResult(request, entry, entry.containsAttribute( assertion, null))); } catch (final ErrorResultException e) { resultHandler.handleErrorResult(e); } } @Override public void handleDelete(final RequestContext requestContext, final DeleteRequest request, final IntermediateResponseHandler intermediateResponseHandler, final ResultHandler resultHandler) { try { final Entry entry; synchronized (writeLock) { final DN dn = request.getName(); entry = getRequiredEntry(request, dn); if (request.getControl(SubtreeDeleteRequestControl.DECODER, decodeOptions) != null) { // Subtree delete. entries.subMap(dn, dn.child(RDN.maxValue())).clear(); } else { // Must be leaf. final DN next = entries.higherKey(dn); if (next == null || !next.isChildOf(dn)) { entries.remove(dn); } else { throw newErrorResult(ResultCode.NOT_ALLOWED_ON_NONLEAF); } } } resultHandler.handleResult(getResult(request, entry, null)); } catch (final DecodeException e) { resultHandler.handleErrorResult(newErrorResult(ResultCode.PROTOCOL_ERROR, e)); } catch (final ErrorResultException e) { resultHandler.handleErrorResult(e); } } @Override public void handleExtendedRequest( final RequestContext requestContext, final ExtendedRequest request, final IntermediateResponseHandler intermediateResponseHandler, final ResultHandler resultHandler) { resultHandler.handleErrorResult(newErrorResult(ResultCode.UNWILLING_TO_PERFORM, "Extended request operation not supported")); } @Override public void handleModify(final RequestContext requestContext, final ModifyRequest request, final IntermediateResponseHandler intermediateResponseHandler, final ResultHandler resultHandler) { try { final Entry entry; final Entry newEntry; synchronized (writeLock) { final DN dn = request.getName(); entry = getRequiredEntry(request, dn); newEntry = new LinkedHashMapEntry(entry); entries.put(dn, modifyEntry(newEntry, request)); } resultHandler.handleResult(getResult(request, entry, newEntry)); } catch (final ErrorResultException e) { resultHandler.handleErrorResult(e); } } @Override public void handleModifyDN(final RequestContext requestContext, final ModifyDNRequest request, final IntermediateResponseHandler intermediateResponseHandler, final ResultHandler resultHandler) { resultHandler.handleErrorResult(newErrorResult(ResultCode.UNWILLING_TO_PERFORM, "ModifyDN request operation not supported")); } @Override public void handleSearch(final RequestContext requestContext, final SearchRequest request, final IntermediateResponseHandler intermediateResponseHandler, final SearchResultHandler resultHandler) { try { final DN dn = request.getName(); final Entry baseEntry = getRequiredEntry(request, dn); final SearchScope scope = request.getScope(); final Filter filter = request.getFilter(); final Matcher matcher = filter.matcher(schema); final AttributeFilter attributeFilter = new AttributeFilter(request.getAttributes(), schema).typesOnly(request .isTypesOnly()); if (scope.equals(SearchScope.BASE_OBJECT)) { if (matcher.matches(baseEntry).toBoolean()) { sendEntry(attributeFilter, resultHandler, baseEntry); } } else if (scope.equals(SearchScope.SINGLE_LEVEL)) { final NavigableMap subtree = entries.subMap(dn, dn.child(RDN.maxValue())); for (final Entry entry : subtree.values()) { // Check for cancellation. requestContext.checkIfCancelled(false); final DN childDN = entry.getName(); if (childDN.isChildOf(dn)) { if (matcher.matches(entry).toBoolean() && !sendEntry(attributeFilter, resultHandler, entry)) { // Caller has asked to stop sending results. break; } } } } else if (scope.equals(SearchScope.WHOLE_SUBTREE)) { final NavigableMap subtree = entries.subMap(dn, dn.child(RDN.maxValue())); for (final Entry entry : subtree.values()) { // Check for cancellation. requestContext.checkIfCancelled(false); if (matcher.matches(entry).toBoolean() && !sendEntry(attributeFilter, resultHandler, entry)) { // Caller has asked to stop sending results. break; } } } else { throw newErrorResult(ResultCode.PROTOCOL_ERROR, "Search request contains an unsupported search scope"); } resultHandler.handleResult(newResult(ResultCode.SUCCESS)); } catch (final ErrorResultException e) { resultHandler.handleErrorResult(e); } } private R addResultControls(final Request request, final Entry before, final Entry after, final R result) throws ErrorResultException { try { // Add pre-read response control if requested. final PreReadRequestControl preRead = request.getControl(PreReadRequestControl.DECODER, decodeOptions); if (preRead != null) { if (preRead.isCritical() && before == null) { throw newErrorResult(ResultCode.UNAVAILABLE_CRITICAL_EXTENSION); } else { final AttributeFilter filter = new AttributeFilter(preRead.getAttributes(), schema); result.addControl(PreReadResponseControl.newControl(filter .filteredViewOf(before))); } } // Add post-read response control if requested. final PostReadRequestControl postRead = request.getControl(PostReadRequestControl.DECODER, decodeOptions); if (postRead != null) { if (postRead.isCritical() && after == null) { throw newErrorResult(ResultCode.UNAVAILABLE_CRITICAL_EXTENSION); } else { final AttributeFilter filter = new AttributeFilter(postRead.getAttributes(), schema); result.addControl(PostReadResponseControl.newControl(filter .filteredViewOf(after))); } } return result; } catch (final DecodeException e) { throw newErrorResult(ResultCode.PROTOCOL_ERROR, e); } } private BindResult getBindResult(final BindRequest request, final Entry before, final Entry after) throws ErrorResultException { return addResultControls(request, before, after, newBindResult(ResultCode.SUCCESS)); } private CompareResult getCompareResult(final CompareRequest request, final Entry entry, final boolean compareResult) throws ErrorResultException { return addResultControls( request, entry, entry, newCompareResult(compareResult ? ResultCode.COMPARE_TRUE : ResultCode.COMPARE_FALSE)); } private Entry getRequiredEntry(final Request request, final DN dn) throws ErrorResultException { final Entry entry = entries.get(dn); if (entry == null) { noSuchObject(dn); } else if (request != null) { AssertionRequestControl control; try { control = request.getControl(AssertionRequestControl.DECODER, decodeOptions); } catch (final DecodeException e) { throw newErrorResult(ResultCode.PROTOCOL_ERROR, e); } if (control != null) { final Filter filter = control.getFilter(); final Matcher matcher = filter.matcher(schema); if (!matcher.matches(entry).toBoolean()) { throw newErrorResult(ResultCode.ASSERTION_FAILED, "The filter '" + filter.toString() + "' did not match the entry '" + entry.getName().toString() + "'"); } } } return entry; } private Result getResult(final Request request, final Entry before, final Entry after) throws ErrorResultException { return addResultControls(request, before, after, newResult(ResultCode.SUCCESS)); } private void noSuchObject(final DN dn) throws ErrorResultException { throw newErrorResult(ResultCode.NO_SUCH_OBJECT, "The entry '" + dn.toString() + "' does not exist"); } private boolean sendEntry(final AttributeFilter filter, final SearchResultHandler resultHandler, final Entry entry) { return resultHandler.handleEntry(newSearchResultEntry(filter.filteredViewOf(entry))); } }