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