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