1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 *
19 */
20 package org.apache.directory.server.ldap.handlers;
21
22
23 import java.util.concurrent.TimeUnit;
24
25 import org.apache.directory.server.core.DirectoryService;
26 import org.apache.directory.server.core.entry.ClonedServerEntry;
27 import org.apache.directory.server.core.entry.ServerStringValue;
28 import org.apache.directory.server.core.event.EventType;
29 import org.apache.directory.server.core.event.NotificationCriteria;
30 import org.apache.directory.server.core.filtering.EntryFilteringCursor;
31 import org.apache.directory.server.core.partition.PartitionNexus;
32 import org.apache.directory.server.ldap.LdapSession;
33 import org.apache.directory.shared.ldap.codec.util.LdapURLEncodingException;
34 import org.apache.directory.shared.ldap.constants.SchemaConstants;
35 import org.apache.directory.shared.ldap.entry.EntryAttribute;
36 import org.apache.directory.shared.ldap.entry.Value;
37 import org.apache.directory.shared.ldap.exception.OperationAbandonedException;
38 import org.apache.directory.shared.ldap.filter.EqualityNode;
39 import org.apache.directory.shared.ldap.filter.OrNode;
40 import org.apache.directory.shared.ldap.filter.PresenceNode;
41 import org.apache.directory.shared.ldap.message.LdapResult;
42 import org.apache.directory.shared.ldap.message.ManageDsaITControl;
43 import org.apache.directory.shared.ldap.message.PersistentSearchControl;
44 import org.apache.directory.shared.ldap.message.ReferralImpl;
45 import org.apache.directory.shared.ldap.message.Response;
46 import org.apache.directory.shared.ldap.message.ResultCodeEnum;
47 import org.apache.directory.shared.ldap.filter.SearchScope;
48 import org.apache.directory.shared.ldap.message.SearchRequest;
49 import org.apache.directory.shared.ldap.message.SearchResponseDone;
50 import org.apache.directory.shared.ldap.message.SearchResponseEntry;
51 import org.apache.directory.shared.ldap.message.SearchResponseEntryImpl;
52 import org.apache.directory.shared.ldap.message.SearchResponseReference;
53 import org.apache.directory.shared.ldap.message.SearchResponseReferenceImpl;
54 import org.apache.directory.shared.ldap.name.LdapDN;
55 import org.apache.directory.shared.ldap.schema.AttributeType;
56 import org.apache.directory.shared.ldap.util.LdapURL;
57 import org.slf4j.Logger;
58 import org.slf4j.LoggerFactory;
59
60 import static org.apache.directory.server.ldap.LdapService.NO_SIZE_LIMIT;
61 import static org.apache.directory.server.ldap.LdapService.NO_TIME_LIMIT;
62
63 import javax.naming.NamingException;
64
65
66 /**
67 * A handler for processing search requests.
68 *
69 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
70 * @version $Rev: 664302 $
71 */
72 public class SearchHandler extends ReferralAwareRequestHandler<SearchRequest>
73 {
74 private static final Logger LOG = LoggerFactory.getLogger( SearchHandler.class );
75
76 /** Speedup for logs */
77 private static final boolean IS_DEBUG = LOG.isDebugEnabled();
78
79 /** cached to save redundant lookups into registries */
80 private AttributeType objectClassAttributeType;
81
82
83 /**
84 * Constructs a new filter EqualityNode asserting that a candidate
85 * objectClass is a referral.
86 *
87 * @param session the {@link LdapSession} to construct the node for
88 * @return the {@link EqualityNode} (objectClass=referral) non-normalized
89 * @throws Exception in the highly unlikely event of schema related failures
90 */
91 private EqualityNode<String> newIsReferralEqualityNode( LdapSession session ) throws Exception
92 {
93 if ( objectClassAttributeType == null )
94 {
95 objectClassAttributeType = session.getCoreSession().getDirectoryService().getRegistries()
96 .getAttributeTypeRegistry().lookup( SchemaConstants.OBJECT_CLASS_AT );
97 }
98
99 EqualityNode<String> ocIsReferral = new EqualityNode<String>( SchemaConstants.OBJECT_CLASS_AT,
100 new ServerStringValue( objectClassAttributeType, SchemaConstants.REFERRAL_OC ) );
101
102 return ocIsReferral;
103 }
104
105
106 /**
107 * Handles search requests containing the persistent search control but
108 * delegates to doSimpleSearch() if the changesOnly parameter of the
109 * control is set to false.
110 *
111 * @param session the LdapSession for which this search is conducted
112 * @param req the search request containing the persistent search control
113 * @param psearchControl the persistent search control extracted
114 * @throws Exception if failures are encountered while searching
115 */
116 private void handlePersistentSearch( LdapSession session, SearchRequest req,
117 PersistentSearchControl psearchControl ) throws Exception
118 {
119 /*
120 * We want the search to complete first before we start listening to
121 * events when the control does NOT specify changes ONLY mode.
122 */
123 if ( ! psearchControl.isChangesOnly() )
124 {
125 SearchResponseDone done = doSimpleSearch( session, req );
126
127 // ok if normal search beforehand failed somehow quickly abandon psearch
128 if ( done.getLdapResult().getResultCode() != ResultCodeEnum.SUCCESS )
129 {
130 session.getIoSession().write( done );
131 return;
132 }
133 }
134
135 if ( req.isAbandoned() )
136 {
137 return;
138 }
139
140 // now we process entries forever as they change
141 PersistentSearchListener handler = new PersistentSearchListener( session, req );
142
143 // compose notification criteria and add the listener to the event
144 // service using that notification criteria to determine which events
145 // are to be delivered to the persistent search issuing client
146 NotificationCriteria criteria = new NotificationCriteria();
147 criteria.setAliasDerefMode( req.getDerefAliases() );
148 criteria.setBase( req.getBase() );
149 criteria.setFilter( req.getFilter() );
150 criteria.setScope( req.getScope() );
151 criteria.setEventMask( EventType.getEventTypes( psearchControl.getChangeTypes() ) );
152 getLdapServer().getDirectoryService().getEventService().addListener( handler, criteria );
153 req.addAbandonListener( new SearchAbandonListener( ldapService, handler ) );
154 return;
155 }
156
157
158 /**
159 * Handles search requests on the RootDSE.
160 *
161 * @param session the LdapSession for which this search is conducted
162 * @param req the search request on the RootDSE
163 * @throws Exception if failures are encountered while searching
164 */
165 private void handleRootDseSearch( LdapSession session, SearchRequest req ) throws Exception
166 {
167 EntryFilteringCursor cursor = null;
168
169 try
170 {
171 cursor = session.getCoreSession().search( req );
172
173 // Position the cursor at the beginning
174 cursor.beforeFirst();
175 boolean hasRootDSE = false;
176
177 while ( cursor.next() )
178 {
179 if ( hasRootDSE )
180 {
181 // This is an error ! We should never find more than one rootDSE !
182 LOG.error( "Got back more than one entry for search on RootDSE which means " +
183 "Cursor is not functioning properly!" );
184 }
185 else
186 {
187 hasRootDSE = true;
188 ClonedServerEntry entry = cursor.get();
189 session.getIoSession().write( generateResponse( session, req, entry ) );
190 }
191 }
192
193 // write the SearchResultDone message
194 session.getIoSession().write( req.getResultResponse() );
195 }
196 finally
197 {
198 // Close the cursor now.
199 if ( cursor != null )
200 {
201 try
202 {
203 cursor.close();
204 }
205 catch ( NamingException e )
206 {
207 LOG.error( "failed on list.close()", e );
208 }
209 }
210 }
211 }
212
213
214 /**
215 * Based on the server maximum time limits configured for search and the
216 * requested time limits this method determines if at all to replace the
217 * default ClosureMonitor of the result set Cursor with one that closes
218 * the Cursor when either server mandated or request mandated time limits
219 * are reached.
220 *
221 * @param req the {@link SearchRequest} issued
222 * @param session the {@link LdapSession} on which search was requested
223 * @param cursor the {@link EntryFilteringCursor} over the search results
224 */
225 private void setTimeLimitsOnCursor( SearchRequest req, LdapSession session, final EntryFilteringCursor cursor )
226 {
227 // Don't bother setting time limits for administrators
228 if ( session.getCoreSession().isAnAdministrator() && req.getTimeLimit() == NO_TIME_LIMIT )
229 {
230 return;
231 }
232
233 /*
234 * Non administrator based searches are limited by time if the server
235 * has been configured with unlimited time and the request specifies
236 * unlimited search time
237 */
238 if ( ldapService.getMaxTimeLimit() == NO_TIME_LIMIT && req.getTimeLimit() == NO_TIME_LIMIT )
239 {
240 return;
241 }
242
243 /*
244 * If the non-administrator user specifies unlimited time but the server
245 * is configured to limit the search time then we limit by the max time
246 * allowed by the configuration
247 */
248 if ( req.getTimeLimit() == 0 )
249 {
250 cursor.setClosureMonitor( new SearchTimeLimitingMonitor( ldapService.getMaxTimeLimit(), TimeUnit.SECONDS ) );
251 return;
252 }
253
254 /*
255 * If the non-administrative user specifies a time limit equal to or
256 * less than the maximum limit configured in the server then we
257 * constrain search by the amount specified in the request
258 */
259 if ( ldapService.getMaxTimeLimit() >= req.getTimeLimit() )
260 {
261 cursor.setClosureMonitor( new SearchTimeLimitingMonitor( req.getTimeLimit(), TimeUnit.SECONDS ) );
262 return;
263 }
264
265 /*
266 * Here the non-administrative user's requested time limit is greater
267 * than what the server's configured maximum limit allows so we limit
268 * the search to the configured limit
269 */
270 cursor.setClosureMonitor( new SearchTimeLimitingMonitor( ldapService.getMaxTimeLimit(), TimeUnit.SECONDS ) );
271 }
272
273
274 private int getSearchSizeLimits( SearchRequest req, LdapSession session )
275 {
276 LOG.debug( "req size limit = {}, configured size limit = {}", req.getSizeLimit(),
277 ldapService.getMaxSizeLimit() );
278
279 // Don't bother setting size limits for administrators that don't ask for it
280 if ( session.getCoreSession().isAnAdministrator() && req.getSizeLimit() == NO_SIZE_LIMIT )
281 {
282 return NO_SIZE_LIMIT;
283 }
284
285 // Don't bother setting size limits for administrators that don't ask for it
286 if ( session.getCoreSession().isAnAdministrator() )
287 {
288 return req.getSizeLimit();
289 }
290
291 /*
292 * Non administrator based searches are limited by size if the server
293 * has been configured with unlimited size and the request specifies
294 * unlimited search size
295 */
296 if ( ldapService.getMaxSizeLimit() == NO_SIZE_LIMIT && req.getSizeLimit() == NO_SIZE_LIMIT )
297 {
298 return NO_SIZE_LIMIT;
299 }
300
301 /*
302 * If the non-administrator user specifies unlimited size but the server
303 * is configured to limit the search size then we limit by the max size
304 * allowed by the configuration
305 */
306 if ( req.getSizeLimit() == 0 )
307 {
308 return ldapService.getMaxSizeLimit();
309 }
310
311 if ( ldapService.getMaxSizeLimit() == NO_SIZE_LIMIT )
312 {
313 return req.getSizeLimit();
314 }
315
316 /*
317 * If the non-administrative user specifies a size limit equal to or
318 * less than the maximum limit configured in the server then we
319 * constrain search by the amount specified in the request
320 */
321 if ( ldapService.getMaxSizeLimit() >= req.getSizeLimit() )
322 {
323 return req.getSizeLimit();
324 }
325
326 /*
327 * Here the non-administrative user's requested size limit is greater
328 * than what the server's configured maximum limit allows so we limit
329 * the search to the configured limit
330 */
331 return ldapService.getMaxSizeLimit();
332 }
333
334
335 /**
336 * Conducts a simple search across the result set returning each entry
337 * back except for the search response done. This is calculated but not
338 * returned so the persistent search mechanism can leverage this method
339 * along with standard search.
340 *
341 * @param session the LDAP session object for this request
342 * @param req the search request
343 * @return the result done
344 * @throws Exception if there are failures while processing the request
345 */
346 private SearchResponseDone doSimpleSearch( LdapSession session, SearchRequest req )
347 throws Exception
348 {
349 /*
350 * Iterate through all search results building and sending back responses
351 * for each search result returned.
352 */
353 EntryFilteringCursor cursor = null;
354
355 try
356 {
357 LdapResult ldapResult = req.getResultResponse().getLdapResult();
358 cursor = session.getCoreSession().search( req );
359 req.addAbandonListener( new SearchAbandonListener( ldapService, cursor ) );
360 setTimeLimitsOnCursor( req, session, cursor );
361 final int sizeLimit = getSearchSizeLimits( req, session );
362 LOG.debug( "using {} for size limit", sizeLimit );
363
364 // Position the cursor at the beginning
365 cursor.beforeFirst();
366
367 if ( sizeLimit == NO_SIZE_LIMIT )
368 {
369 while ( cursor.next() )
370 {
371 if ( session.getIoSession().isClosing() )
372 {
373 break;
374 }
375 ClonedServerEntry entry = cursor.get();
376 session.getIoSession().write( generateResponse( session, req, entry ) );
377 }
378 }
379 else
380 {
381 int count = 0;
382 while ( cursor.next() )
383 {
384 if ( session.getIoSession().isClosing() )
385 {
386 break;
387 }
388 if ( count < sizeLimit )
389 {
390 ClonedServerEntry entry = cursor.get();
391 session.getIoSession().write( generateResponse( session, req, entry ) );
392 count++;
393 }
394 else
395 {
396 // DO NOT WRITE THE RESPONSE - JUST RETURN IT
397 ldapResult.setResultCode( ResultCodeEnum.SIZE_LIMIT_EXCEEDED );
398 return ( SearchResponseDone ) req.getResultResponse();
399 }
400 }
401 }
402
403 // DO NOT WRITE THE RESPONSE - JUST RETURN IT
404 ldapResult.setResultCode( ResultCodeEnum.SUCCESS );
405 return ( SearchResponseDone ) req.getResultResponse();
406 }
407 finally
408 {
409 if ( cursor != null )
410 {
411 try
412 {
413 cursor.close();
414 }
415 catch ( NamingException e )
416 {
417 LOG.error( "failed on list.close()", e );
418 }
419 }
420 }
421 }
422
423
424 /**
425 * Generates a response for an entry retrieved from the server core based
426 * on the nature of the request with respect to referral handling. This
427 * method will either generate a SearchResponseEntry or a
428 * SearchResponseReference depending on if the entry is a referral or if
429 * the ManageDSAITControl has been enabled.
430 *
431 * @param req the search request
432 * @param entry the entry to be handled
433 * @return the response for the entry
434 * @throws Exception if there are problems in generating the response
435 */
436 private Response generateResponse( LdapSession session, SearchRequest req, ClonedServerEntry entry ) throws Exception
437 {
438 EntryAttribute ref = entry.getOriginalEntry().get( SchemaConstants.REF_AT );
439 boolean hasManageDsaItControl = req.getControls().containsKey( ManageDsaITControl.CONTROL_OID );
440
441 if ( ref != null && ! hasManageDsaItControl )
442 {
443 SearchResponseReference respRef;
444 respRef = new SearchResponseReferenceImpl( req.getMessageId() );
445 respRef.setReferral( new ReferralImpl() );
446
447 for ( Value<?> val : ref )
448 {
449 String url = ( String ) val.get();
450
451 if ( ! url.startsWith( "ldap" ) )
452 {
453 respRef.getReferral().addLdapUrl( url );
454 }
455
456 LdapURL ldapUrl = new LdapURL();
457 ldapUrl.setForceScopeRendering( true );
458 try
459 {
460 ldapUrl.parse( url.toCharArray() );
461 }
462 catch ( LdapURLEncodingException e )
463 {
464 LOG.error( "Bad URL ({}) for ref in {}. Reference will be ignored.", url, entry );
465 }
466
467 switch( req.getScope() )
468 {
469 case SUBTREE:
470 ldapUrl.setScope( SearchScope.SUBTREE.getJndiScope() );
471 break;
472 case ONELEVEL: // one level here is object level on remote server
473 ldapUrl.setScope( SearchScope.OBJECT.getJndiScope() );
474 break;
475 default:
476 throw new IllegalStateException( "Unexpected base scope." );
477 }
478
479 respRef.getReferral().addLdapUrl( ldapUrl.toString() );
480 }
481
482 return respRef;
483 }
484 else
485 {
486 SearchResponseEntry respEntry;
487 respEntry = new SearchResponseEntryImpl( req.getMessageId() );
488 respEntry.setEntry( entry );
489 respEntry.setObjectName( entry.getDn() );
490
491 return respEntry;
492 }
493 }
494
495
496 /**
497 * Alters the filter expression based on the presence of the
498 * ManageDsaIT control. If the control is not present, the search
499 * filter will be altered to become a disjunction with two terms.
500 * The first term is the original filter. The second term is a
501 * (objectClass=referral) assertion. When OR'd together these will
502 * make sure we get all referrals so we can process continuations
503 * properly without having the filter remove them from the result
504 * set.
505 *
506 * NOTE: original filter is first since most entries are not referrals
507 * so it has a higher probability on average of accepting and shorting
508 * evaluation before having to waste cycles trying to evaluate if the
509 * entry is a referral.
510 *
511 * @param session the session to use to construct the filter (schema access)
512 * @param req the request to get the original filter from
513 * @throws Exception if there are schema access problems
514 */
515 public void modifyFilter( LdapSession session, SearchRequest req ) throws Exception
516 {
517 if ( req.hasControl( ManageDsaITControl.CONTROL_OID ) )
518 {
519 return;
520 }
521
522 /*
523 * Do not add the OR'd (objectClass=referral) expression if the user
524 * searches for the subSchemaSubEntry as the SchemaIntercepter can't
525 * handle an OR'd filter.
526 */
527 if ( isSubSchemaSubEntrySearch( session, req ) )
528 {
529 return;
530 }
531
532 /*
533 * Most of the time the search filter is just (objectClass=*) and if
534 * this is the case then there's no reason at all to OR this with an
535 * (objectClass=referral). If we detect this case then we leave it
536 * as is to represent the OR condition:
537 *
538 * (| (objectClass=referral)(objectClass=*)) == (objectClass=*)
539 */
540 if ( req.getFilter() instanceof PresenceNode )
541 {
542 PresenceNode presenceNode = ( PresenceNode ) req.getFilter();
543
544 AttributeType at = session.getCoreSession().getDirectoryService()
545 .getRegistries().getAttributeTypeRegistry().lookup( presenceNode.getAttribute() );
546 if ( at.getOid().equals( SchemaConstants.OBJECT_CLASS_AT_OID ) )
547 {
548 return;
549 }
550 }
551
552 // using varags to add two expressions to an OR node
553 req.setFilter( new OrNode( req.getFilter(), newIsReferralEqualityNode( session ) ) );
554 }
555
556
557 /**
558 * Main message handing method for search requests. This will be called
559 * even if the ManageDsaIT control is present because the super class does
560 * not know that the search operation has more to do after finding the
561 * base. The call to this means that finding the base can ignore
562 * referrals.
563 *
564 * @param session the associated session
565 * @param req the received SearchRequest
566 */
567 public void handleIgnoringReferrals( LdapSession session, LdapDN reqTargetDn,
568 ClonedServerEntry entry, SearchRequest req )
569 {
570 if ( IS_DEBUG )
571 {
572 LOG.debug( "Message received: {}", req.toString() );
573 }
574
575 // add the search request to the registry of outstanding requests for this session
576 session.registerOutstandingRequest( req );
577
578 try
579 {
580 // modify the filter to affect continuation support
581 modifyFilter( session, req );
582
583 // ===============================================================
584 // Handle search in rootDSE differently.
585 // ===============================================================
586 if ( isRootDSESearch( req ) )
587 {
588 handleRootDseSearch( session, req );
589 return;
590 }
591
592 // ===============================================================
593 // Handle psearch differently
594 // ===============================================================
595
596 PersistentSearchControl psearchControl = ( PersistentSearchControl )
597 req.getControls().get( PersistentSearchControl.CONTROL_OID );
598
599 if ( psearchControl != null )
600 {
601 handlePersistentSearch( session, req, psearchControl );
602
603 // do not unregister the outstanding request unlike below
604 return;
605 }
606
607 // ===============================================================
608 // Handle regular search requests from here down
609 // ===============================================================
610
611 SearchResponseDone done = doSimpleSearch( session, req );
612 session.getIoSession().write( done );
613 session.unregisterOutstandingRequest( req );
614 }
615 catch ( Exception e )
616 {
617 /*
618 * From RFC 2251 Section 4.11:
619 *
620 * In the event that a server receives an Abandon Request on a Search
621 * operation in the midst of transmitting responses to the Search, that
622 * server MUST cease transmitting entry responses to the abandoned
623 * request immediately, and MUST NOT send the SearchResultDone. Of
624 * course, the server MUST ensure that only properly encoded LDAPMessage
625 * PDUs are transmitted.
626 *
627 * SO DON'T SEND BACK ANYTHING!!!!!
628 */
629 if ( e instanceof OperationAbandonedException )
630 {
631 return;
632 }
633
634 handleException( session, req, e );
635 }
636 }
637
638
639 /**
640 * Determines if a search request is on the RootDSE of the server.
641 *
642 * It is a RootDSE search if :
643 * - the base DN is empty
644 * - and the scope is BASE OBJECT
645 * - and the filter is (ObjectClass = *)
646 *
647 * (RFC 4511, 5.1, par. 1 & 2)
648 *
649 * @param req the request issued
650 * @return true if the search is on the RootDSE false otherwise
651 */
652 private static boolean isRootDSESearch( SearchRequest req )
653 {
654 boolean isBaseIsRoot = req.getBase().isEmpty();
655 boolean isBaseScope = req.getScope() == SearchScope.OBJECT;
656 boolean isRootDSEFilter = false;
657
658 if ( req.getFilter() instanceof PresenceNode )
659 {
660 String attribute = ( ( PresenceNode ) req.getFilter() ).getAttribute();
661 isRootDSEFilter = attribute.equalsIgnoreCase( SchemaConstants.OBJECT_CLASS_AT ) ||
662 attribute.equals( SchemaConstants.OBJECT_CLASS_AT_OID );
663 }
664
665 return isBaseIsRoot && isBaseScope && isRootDSEFilter;
666 }
667
668
669 /**
670 * <p>
671 * Determines if a search request is a subSchemaSubEntry search.
672 * </p>
673 * <p>
674 * It is a schema search if:
675 * - the base DN is the DN of the subSchemaSubEntry of the root DSE
676 * - and the scope is BASE OBJECT
677 * - and the filter is (objectClass=subschema)
678 * (RFC 4512, 4.4,)
679 * </p>
680 * <p>
681 * However in this method we only check the first condition to avoid
682 * performance issues.
683 * </p>
684 *
685 * @param session the LDAP session
686 * @param req the request issued
687 *
688 * @return true if the search is on the subSchemaSubEntry, false otherwise
689 *
690 * @throws Exception the exception
691 */
692 private static boolean isSubSchemaSubEntrySearch( LdapSession session, SearchRequest req ) throws Exception
693 {
694 LdapDN base = req.getBase();
695 String baseNormForm = ( base.isNormalized() ? base.getNormName() : base.toNormName() );
696
697 DirectoryService ds = session.getCoreSession().getDirectoryService();
698 PartitionNexus nexus = ds.getPartitionNexus();
699 Value<?> subschemaSubentry = nexus.getRootDSE( null ).get( SchemaConstants.SUBSCHEMA_SUBENTRY_AT ).get();
700 LdapDN subschemaSubentryDn = new LdapDN( ( String ) ( subschemaSubentry.get() ) );
701 subschemaSubentryDn.normalize( ds.getRegistries().getAttributeTypeRegistry().getNormalizerMapping() );
702 String subschemaSubentryDnNorm = subschemaSubentryDn.getNormName();
703
704 return subschemaSubentryDnNorm.equals( baseNormForm );
705 }
706 }