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 * .<LDAPClientContext> 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}