001/*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License").  You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *      Copyright 2013-2015 ForgeRock AS.
024 */
025package org.forgerock.opendj.ldap;
026
027import static org.forgerock.opendj.ldap.Attributes.singletonAttribute;
028import static org.forgerock.opendj.ldap.Entries.modifyEntry;
029import static org.forgerock.opendj.ldap.LdapException.newLdapException;
030import static org.forgerock.opendj.ldap.responses.Responses.newBindResult;
031import static org.forgerock.opendj.ldap.responses.Responses.newCompareResult;
032import static org.forgerock.opendj.ldap.responses.Responses.newResult;
033import static org.forgerock.opendj.ldap.responses.Responses.newSearchResultEntry;
034
035import java.io.IOException;
036import java.util.Collection;
037import java.util.Map;
038import java.util.concurrent.ConcurrentSkipListMap;
039
040import org.forgerock.i18n.LocalizedIllegalArgumentException;
041import org.forgerock.opendj.ldap.controls.AssertionRequestControl;
042import org.forgerock.opendj.ldap.controls.PostReadRequestControl;
043import org.forgerock.opendj.ldap.controls.PostReadResponseControl;
044import org.forgerock.opendj.ldap.controls.PreReadRequestControl;
045import org.forgerock.opendj.ldap.controls.PreReadResponseControl;
046import org.forgerock.opendj.ldap.controls.SimplePagedResultsControl;
047import org.forgerock.opendj.ldap.controls.SubtreeDeleteRequestControl;
048import org.forgerock.opendj.ldap.requests.AddRequest;
049import org.forgerock.opendj.ldap.requests.BindRequest;
050import org.forgerock.opendj.ldap.requests.CompareRequest;
051import org.forgerock.opendj.ldap.requests.DeleteRequest;
052import org.forgerock.opendj.ldap.requests.ExtendedRequest;
053import org.forgerock.opendj.ldap.requests.GenericBindRequest;
054import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
055import org.forgerock.opendj.ldap.requests.ModifyRequest;
056import org.forgerock.opendj.ldap.requests.Request;
057import org.forgerock.opendj.ldap.requests.SearchRequest;
058import org.forgerock.opendj.ldap.requests.SimpleBindRequest;
059import org.forgerock.opendj.ldap.responses.BindResult;
060import org.forgerock.opendj.ldap.responses.CompareResult;
061import org.forgerock.opendj.ldap.responses.ExtendedResult;
062import org.forgerock.opendj.ldap.responses.Result;
063import org.forgerock.opendj.ldap.schema.Schema;
064import org.forgerock.opendj.ldif.EntryReader;
065
066/**
067 * A simple in memory back-end which can be used for testing. It is not intended
068 * for production use due to various limitations. The back-end implementations
069 * supports the following:
070 * <ul>
071 * <li>add, bind (simple), compare, delete, modify, and search operations, but
072 * not modifyDN nor extended operations
073 * <li>assertion, pre-, and post- read controls, subtree delete control, and
074 * permissive modify control
075 * <li>thread safety - supports concurrent operations
076 * </ul>
077 * It does not support the following:
078 * <ul>
079 * <li>high performance
080 * <li>secure password storage
081 * <li>schema checking
082 * <li>persistence
083 * <li>indexing
084 * </ul>
085 * This class can be used in conjunction with the factories defined in
086 * {@link Connections} to create simple servers as well as mock LDAP
087 * connections. For example, to create a mock LDAP connection factory:
088 *
089 * <pre>
090 * MemoryBackend backend = new MemoryBackend();
091 * Connection connection = newInternalConnectionFactory(newServerConnectionFactory(backend), null)
092 *         .getConnection();
093 * </pre>
094 *
095 * To create a simple LDAP server listening on 0.0.0.0:1389:
096 *
097 * <pre>
098 * MemoryBackend backend = new MemoryBackend();
099 * LDAPListener listener = new LDAPListener(1389, Connections
100 *         .&lt;LDAPClientContext&gt; newServerConnectionFactory(backend));
101 * </pre>
102 */
103public final class MemoryBackend implements RequestHandler<RequestContext> {
104    private final DecodeOptions decodeOptions;
105    private final ConcurrentSkipListMap<DN, Entry> entries = new ConcurrentSkipListMap<>();
106    private final Schema schema;
107    private final Object writeLock = new Object();
108
109    /**
110     * Creates a new empty memory backend which will use the default schema.
111     */
112    public MemoryBackend() {
113        this(Schema.getDefaultSchema());
114    }
115
116    /**
117     * Creates a new memory backend which will use the default schema, and will
118     * contain the entries read from the provided entry reader.
119     *
120     * @param reader
121     *            The entry reader.
122     * @throws IOException
123     *             If an unexpected IO error occurred while reading the entries,
124     *             or if duplicate entries are detected.
125     */
126    public MemoryBackend(final EntryReader reader) throws IOException {
127        this(Schema.getDefaultSchema(), reader);
128    }
129
130    /**
131     * Creates a new empty memory backend which will use the provided schema.
132     *
133     * @param schema
134     *            The schema to use for decoding filters, etc.
135     */
136    public MemoryBackend(final Schema schema) {
137        this.schema = schema;
138        this.decodeOptions = new DecodeOptions().setSchema(schema);
139    }
140
141    /**
142     * Creates a new memory backend which will use the provided schema, and will
143     * contain the entries read from the provided entry reader.
144     *
145     * @param schema
146     *            The schema to use for decoding filters, etc.
147     * @param reader
148     *            The entry reader.
149     * @throws IOException
150     *             If an unexpected IO error occurred while reading the entries,
151     *             or if duplicate entries are detected.
152     */
153    public MemoryBackend(final Schema schema, final EntryReader reader) throws IOException {
154        this.schema = schema;
155        this.decodeOptions = new DecodeOptions().setSchema(schema);
156        load(reader, false);
157    }
158
159    /**
160     * Clears the contents of this memory backend so that it does not contain
161     * any entries.
162     *
163     * @return This memory backend.
164     */
165    public MemoryBackend clear() {
166        synchronized (writeLock) {
167            entries.clear();
168        }
169        return this;
170    }
171
172    /**
173     * Returns {@code true} if the named entry exists in this memory backend.
174     *
175     * @param dn
176     *            The name of the entry.
177     * @return {@code true} if the named entry exists in this memory backend.
178     */
179    public boolean contains(final DN dn) {
180        return get(dn) != null;
181    }
182
183    /**
184     * Returns {@code true} if the named entry exists in this memory backend.
185     *
186     * @param dn
187     *            The name of the entry.
188     * @return {@code true} if the named entry exists in this memory backend.
189     */
190    public boolean contains(final String dn) {
191        return get(dn) != null;
192    }
193
194    /**
195     * Returns the named entry contained in this memory backend, or {@code null}
196     * if it does not exist.
197     *
198     * @param dn
199     *            The name of the entry to be returned.
200     * @return The named entry.
201     */
202    public Entry get(final DN dn) {
203        return entries.get(dn);
204    }
205
206    /**
207     * Returns the named entry contained in this memory backend, or {@code null}
208     * if it does not exist.
209     *
210     * @param dn
211     *            The name of the entry to be returned.
212     * @return The named entry.
213     */
214    public Entry get(final String dn) {
215        return get(DN.valueOf(dn, schema));
216    }
217
218    /**
219     * Returns a collection containing all of the entries in this memory
220     * backend. The returned collection is backed by this memory backend, so
221     * changes to the collection are reflected in this memory backend and
222     * vice-versa. The returned collection supports entry removal, iteration,
223     * and is thread safe, but it does not support addition of new entries.
224     *
225     * @return A collection containing all of the entries in this memory
226     *         backend.
227     */
228    public Collection<Entry> getAll() {
229        return entries.values();
230    }
231
232    @Override
233    public void handleAdd(final RequestContext requestContext, final AddRequest request,
234            final IntermediateResponseHandler intermediateResponseHandler,
235            final LdapResultHandler<Result> resultHandler) {
236        try {
237            synchronized (writeLock) {
238                final DN dn = request.getName();
239                final DN parent = dn.parent();
240                if (entries.containsKey(dn)) {
241                    throw newLdapException(ResultCode.ENTRY_ALREADY_EXISTS, "The entry '" + dn + "' already exists");
242                } else if (parent != null && !entries.containsKey(parent)) {
243                    noSuchObject(parent);
244                } else {
245                    entries.put(dn, request);
246                }
247            }
248            resultHandler.handleResult(getResult(request, null, request));
249        } catch (final LdapException e) {
250            resultHandler.handleException(e);
251        }
252    }
253
254    @Override
255    public void handleBind(final RequestContext requestContext, final int version,
256            final BindRequest request,
257            final IntermediateResponseHandler intermediateResponseHandler,
258            final LdapResultHandler<BindResult> resultHandler) {
259        try {
260            final Entry entry;
261            synchronized (writeLock) {
262                final DN username = DN.valueOf(request.getName(), schema);
263                final byte[] password;
264                if (request instanceof SimpleBindRequest) {
265                    password = ((SimpleBindRequest) request).getPassword();
266                } else if (request instanceof GenericBindRequest
267                        && request.getAuthenticationType() == BindRequest.AUTHENTICATION_TYPE_SIMPLE) {
268                    password = ((GenericBindRequest) request).getAuthenticationValue();
269                } else {
270                    throw newLdapException(ResultCode.PROTOCOL_ERROR,
271                            "non-SIMPLE authentication not supported: " + request.getAuthenticationType());
272                }
273                entry = getRequiredEntry(null, username);
274                if (!entry.containsAttribute("userPassword", password)) {
275                    throw newLdapException(ResultCode.INVALID_CREDENTIALS, "Wrong password");
276                }
277            }
278            resultHandler.handleResult(getBindResult(request, entry, entry));
279        } catch (final LocalizedIllegalArgumentException e) {
280            resultHandler.handleException(newLdapException(ResultCode.PROTOCOL_ERROR, e));
281        } catch (final EntryNotFoundException e) {
282            /*
283             * Usually you would not include a diagnostic message, but we'll add
284             * one here because the memory back-end is not intended for
285             * production use.
286             */
287            resultHandler.handleException(newLdapException(ResultCode.INVALID_CREDENTIALS, "Unknown user"));
288        } catch (final LdapException e) {
289            resultHandler.handleException(e);
290        }
291    }
292
293    @Override
294    public void handleCompare(final RequestContext requestContext, final CompareRequest request,
295            final IntermediateResponseHandler intermediateResponseHandler,
296            final LdapResultHandler<CompareResult> resultHandler) {
297        try {
298            final Entry entry;
299            final Attribute assertion;
300            synchronized (writeLock) {
301                final DN dn = request.getName();
302                entry = getRequiredEntry(request, dn);
303                assertion =
304                        singletonAttribute(request.getAttributeDescription(), request
305                                .getAssertionValue());
306            }
307            resultHandler.handleResult(getCompareResult(request, entry, entry.containsAttribute(
308                    assertion, null)));
309        } catch (final LdapException e) {
310            resultHandler.handleException(e);
311        }
312    }
313
314    @Override
315    public void handleDelete(final RequestContext requestContext, final DeleteRequest request,
316            final IntermediateResponseHandler intermediateResponseHandler,
317            final LdapResultHandler<Result> resultHandler) {
318        try {
319            final Entry entry;
320            synchronized (writeLock) {
321                final DN dn = request.getName();
322                entry = getRequiredEntry(request, dn);
323                if (request.getControl(SubtreeDeleteRequestControl.DECODER, decodeOptions) != null) {
324                    // Subtree delete.
325                    entries.subMap(dn, dn.child(RDN.maxValue())).clear();
326                } else {
327                    // Must be leaf.
328                    final DN next = entries.higherKey(dn);
329                    if (next == null || !next.isChildOf(dn)) {
330                        entries.remove(dn);
331                    } else {
332                        throw newLdapException(ResultCode.NOT_ALLOWED_ON_NONLEAF);
333                    }
334                }
335            }
336            resultHandler.handleResult(getResult(request, entry, null));
337        } catch (final DecodeException e) {
338            resultHandler.handleException(newLdapException(ResultCode.PROTOCOL_ERROR, e));
339        } catch (final LdapException e) {
340            resultHandler.handleException(e);
341        }
342    }
343
344    @Override
345    public <R extends ExtendedResult> void handleExtendedRequest(
346            final RequestContext requestContext, final ExtendedRequest<R> request,
347            final IntermediateResponseHandler intermediateResponseHandler,
348            final LdapResultHandler<R> resultHandler) {
349        resultHandler.handleException(newLdapException(ResultCode.UNWILLING_TO_PERFORM,
350                "Extended request operation not supported"));
351    }
352
353    @Override
354    public void handleModify(final RequestContext requestContext, final ModifyRequest request,
355            final IntermediateResponseHandler intermediateResponseHandler,
356            final LdapResultHandler<Result> resultHandler) {
357        try {
358            final Entry entry;
359            final Entry newEntry;
360            synchronized (writeLock) {
361                final DN dn = request.getName();
362                entry = getRequiredEntry(request, dn);
363                newEntry = new LinkedHashMapEntry(entry);
364                entries.put(dn, modifyEntry(newEntry, request));
365            }
366            resultHandler.handleResult(getResult(request, entry, newEntry));
367        } catch (final LdapException e) {
368            resultHandler.handleException(e);
369        }
370    }
371
372    @Override
373    public void handleModifyDN(final RequestContext requestContext, final ModifyDNRequest request,
374            final IntermediateResponseHandler intermediateResponseHandler,
375            final LdapResultHandler<Result> resultHandler) {
376        resultHandler.handleException(newLdapException(ResultCode.UNWILLING_TO_PERFORM,
377                "ModifyDN request operation not supported"));
378    }
379
380    @Override
381    public void handleSearch(final RequestContext requestContext, final SearchRequest request,
382        final IntermediateResponseHandler intermediateResponseHandler, final SearchResultHandler entryHandler,
383        LdapResultHandler<Result> resultHandler) {
384        try {
385            final DN dn = request.getName();
386            final SearchScope scope = request.getScope();
387            final Filter filter = request.getFilter();
388            final Matcher matcher = filter.matcher(schema);
389            final AttributeFilter attributeFilter =
390                new AttributeFilter(request.getAttributes(), schema).typesOnly(request.isTypesOnly());
391            if (scope.equals(SearchScope.BASE_OBJECT)) {
392                final Entry baseEntry = getRequiredEntry(request, dn);
393                if (matcher.matches(baseEntry).toBoolean()) {
394                    sendEntry(attributeFilter, entryHandler, baseEntry);
395                }
396                resultHandler.handleResult(newResult(ResultCode.SUCCESS));
397            } else if (scope.equals(SearchScope.SINGLE_LEVEL) || scope.equals(SearchScope.SUBORDINATES)
398                || scope.equals(SearchScope.WHOLE_SUBTREE)) {
399                searchWithSubordinates(requestContext, entryHandler, resultHandler, dn, matcher, attributeFilter,
400                    request.getSizeLimit(), scope,
401                    request.getControl(SimplePagedResultsControl.DECODER, new DecodeOptions()));
402            } else {
403                throw newLdapException(ResultCode.PROTOCOL_ERROR,
404                        "Search request contains an unsupported search scope");
405            }
406        } catch (DecodeException e) {
407            resultHandler.handleException(newLdapException(ResultCode.PROTOCOL_ERROR, e.getMessage(), e));
408        } catch (final LdapException e) {
409            resultHandler.handleException(e);
410        }
411    }
412
413    /**
414     * Returns {@code true} if this memory backend does not contain any entries.
415     *
416     * @return {@code true} if this memory backend does not contain any entries.
417     */
418    public boolean isEmpty() {
419        return entries.isEmpty();
420    }
421
422    /**
423     * Reads all of the entries from the provided entry reader and adds them to
424     * the content of this memory backend.
425     *
426     * @param reader
427     *            The entry reader.
428     * @param overwrite
429     *            {@code true} if existing entries should be replaced, or
430     *            {@code false} if an error should be returned when duplicate
431     *            entries are encountered.
432     * @return This memory backend.
433     * @throws IOException
434     *             If an unexpected IO error occurred while reading the entries,
435     *             or if duplicate entries are detected and {@code overwrite} is
436     *             {@code false}.
437     */
438    public MemoryBackend load(final EntryReader reader, final boolean overwrite) throws IOException {
439        synchronized (writeLock) {
440            if (reader != null) {
441                try {
442                    while (reader.hasNext()) {
443                        final Entry entry = reader.readEntry();
444                        final DN dn = entry.getName();
445                        if (!overwrite && entries.containsKey(dn)) {
446                            throw newLdapException(ResultCode.ENTRY_ALREADY_EXISTS,
447                                    "Attempted to add the entry '" + dn + "' multiple times");
448                        } else {
449                            entries.put(dn, entry);
450                        }
451                    }
452                } finally {
453                    reader.close();
454                }
455            }
456        }
457        return this;
458    }
459
460    /**
461     * Returns the number of entries contained in this memory backend.
462     *
463     * @return The number of entries contained in this memory backend.
464     */
465    public int size() {
466        return entries.size();
467    }
468
469    /**
470     * Perform a search for scope that includes subordinates, i.e., either
471     * <code>SearchScope.SINGLE_LEVEL</code> or <code>SearchScope.WHOLE_SUBTREE</code>.
472     *
473     * @param requestContext context of this request
474     * @param resultHandler handler which should be used to send back the search results to the client.
475     * @param dn distinguished name of the base entry used for this request
476     * @param matcher to filter entries that matches this request
477     * @param attributeFilter to select attributes to return in search results
478     * @param sizeLimit maximum number of entries to return. A value of zero indicates no restriction
479     *          on number of entries.
480     * @param pagedResults The simple paged results control, if present.
481     * @throws CancelledResultException
482     *           If a cancellation request has been received and processing of
483     *           the request should be aborted if possible.
484     * @throws LdapException
485     *           If the request is unsuccessful.
486     */
487    private void searchWithSubordinates(final RequestContext requestContext, final SearchResultHandler entryHandler,
488            final LdapResultHandler<Result> resultHandler, final DN dn, final Matcher matcher,
489            final AttributeFilter attributeFilter, final int sizeLimit, SearchScope scope,
490            SimplePagedResultsControl pagedResults) throws CancelledResultException, LdapException {
491        final int pageSize = pagedResults != null ? pagedResults.getSize() : 0;
492        final int offset = (pagedResults != null && !pagedResults.getCookie().isEmpty())
493                ? Integer.valueOf(pagedResults.getCookie().toString()) : 0;
494        final Map<DN, Entry> subtree = entries.subMap(dn, dn.child(RDN.maxValue()));
495        int numberOfResults = 0;
496        int position = 0;
497        for (final Entry entry : subtree.values()) {
498            requestContext.checkIfCancelled(false);
499            if (scope.equals(SearchScope.WHOLE_SUBTREE) || entry.getName().isChildOf(dn)
500                    || (scope.equals(SearchScope.SUBORDINATES) && !entry.getName().equals(dn))) {
501                if (matcher.matches(entry).toBoolean()) {
502                    /*
503                     * This entry is going to be returned to the client so it
504                     * counts towards the size limit and any paging criteria.
505                     */
506
507                    // Check size limit.
508                    if (sizeLimit > 0 && numberOfResults >= sizeLimit) {
509                        throw newLdapException(newResult(ResultCode.SIZE_LIMIT_EXCEEDED));
510                    }
511
512                    // Ignore this entry if we haven't reached the first page yet.
513                    if (pageSize > 0 && position++ < offset) {
514                        continue;
515                    }
516
517                    // Send the entry back to the client.
518                    if (!sendEntry(attributeFilter, entryHandler, entry)) {
519                        // Client has disconnected or cancelled.
520                        break;
521                    }
522
523                    numberOfResults++;
524
525                    // Stop if we've reached the end of the page.
526                    if (pageSize > 0 && numberOfResults == pageSize) {
527                        break;
528                    }
529                }
530            }
531        }
532        final Result result = newResult(ResultCode.SUCCESS);
533        if (pageSize > 0) {
534            final ByteString cookie = numberOfResults == pageSize ? ByteString.valueOf(String.valueOf(position))
535                    : ByteString.empty();
536            result.addControl(SimplePagedResultsControl.newControl(true, 0, cookie));
537        }
538        resultHandler.handleResult(result);
539    }
540
541    private <R extends Result> R addResultControls(final Request request, final Entry before,
542            final Entry after, final R result) throws LdapException {
543        try {
544            // Add pre-read response control if requested.
545            final PreReadRequestControl preRead =
546                    request.getControl(PreReadRequestControl.DECODER, decodeOptions);
547            if (preRead != null) {
548                if (preRead.isCritical() && before == null) {
549                    throw newLdapException(ResultCode.UNAVAILABLE_CRITICAL_EXTENSION);
550                } else {
551                    final AttributeFilter filter =
552                            new AttributeFilter(preRead.getAttributes(), schema);
553                    result.addControl(PreReadResponseControl.newControl(filter
554                            .filteredViewOf(before)));
555                }
556            }
557
558            // Add post-read response control if requested.
559            final PostReadRequestControl postRead =
560                    request.getControl(PostReadRequestControl.DECODER, decodeOptions);
561            if (postRead != null) {
562                if (postRead.isCritical() && after == null) {
563                    throw newLdapException(ResultCode.UNAVAILABLE_CRITICAL_EXTENSION);
564                } else {
565                    final AttributeFilter filter =
566                            new AttributeFilter(postRead.getAttributes(), schema);
567                    result.addControl(PostReadResponseControl.newControl(filter
568                            .filteredViewOf(after)));
569                }
570            }
571            return result;
572        } catch (final DecodeException e) {
573            throw newLdapException(ResultCode.PROTOCOL_ERROR, e);
574        }
575    }
576
577    private BindResult getBindResult(final BindRequest request, final Entry before,
578            final Entry after) throws LdapException {
579        return addResultControls(request, before, after, newBindResult(ResultCode.SUCCESS));
580    }
581
582    private CompareResult getCompareResult(final CompareRequest request, final Entry entry,
583            final boolean compareResult) throws LdapException {
584        return addResultControls(
585                request,
586                entry,
587                entry,
588                newCompareResult(compareResult ? ResultCode.COMPARE_TRUE : ResultCode.COMPARE_FALSE));
589    }
590
591    private Entry getRequiredEntry(final Request request, final DN dn) throws LdapException {
592        final Entry entry = entries.get(dn);
593        if (entry == null) {
594            noSuchObject(dn);
595        } else if (request != null) {
596            AssertionRequestControl control;
597            try {
598                control = request.getControl(AssertionRequestControl.DECODER, decodeOptions);
599            } catch (final DecodeException e) {
600                throw newLdapException(ResultCode.PROTOCOL_ERROR, e);
601            }
602            if (control != null) {
603                final Filter filter = control.getFilter();
604                final Matcher matcher = filter.matcher(schema);
605                if (!matcher.matches(entry).toBoolean()) {
606                    throw newLdapException(ResultCode.ASSERTION_FAILED,
607                            "The filter '" + filter + "' did not match the entry '" + entry.getName() + "'");
608                }
609            }
610        }
611        return entry;
612    }
613
614    private Result getResult(final Request request, final Entry before, final Entry after) throws LdapException {
615        return addResultControls(request, before, after, newResult(ResultCode.SUCCESS));
616    }
617
618    private void noSuchObject(final DN dn) throws LdapException {
619        throw newLdapException(ResultCode.NO_SUCH_OBJECT, "The entry '" + dn + "' does not exist");
620    }
621
622    private boolean sendEntry(final AttributeFilter filter,
623            final SearchResultHandler resultHandler, final Entry entry) {
624        return resultHandler.handleEntry(newSearchResultEntry(filter.filteredViewOf(entry)));
625    }
626}