Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,21 @@ public interface DirectoryListener
void entryDeleted( DeleteOperationContext deleteContext );


/**
* Called synchronously, before an entry is deleted, for every listener which
* will be notified of the deletion. As the entry still exists at this point,
* this is the last chance for the listener to read it on behalf of its own
* session (for instance to check that this session is allowed to see it).
* The default implementation does nothing.
*
* @param deleteContext the delete operation context responsible for the change
*/
default void beforeEntryDeleted( DeleteOperationContext deleteContext )
{
// Nothing to do by default
}


/**
* Called when an entry has been modified.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -96,7 +96,6 @@ public void init( DirectoryService directoryService ) throws LdapException
super.init( directoryService );

evaluator = new ExpressionEvaluator( schemaManager );
executor = new ThreadPoolExecutor( 1, 10, 1000, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>( 100 ) );

ThreadFactory threadFactory = new ThreadFactory()
{
Expand All @@ -110,8 +109,13 @@ public Thread newThread( Runnable runnable )
}
};

executor = new ThreadPoolExecutor( 1, 10, 1000, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>( 100 ),
threadFactory );
// The listeners may have to wait for the end of the operation which notifies them
// (persistent search listeners read the modified entry), so the queue must not be
// bounded : a rejected notification would make this operation fail.
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( 10, 10, 1000, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(), threadFactory );
threadPoolExecutor.allowCoreThreadTimeOut( true );
executor = threadPoolExecutor;

this.directoryService.setEventService( new DefaultEventService( directoryService ) );
LOG.info( "Initialization complete." );
Expand Down Expand Up @@ -276,22 +280,25 @@ public void add( final AddOperationContext addContext ) throws LdapException
@Override
public void delete( final DeleteOperationContext deleteContext ) throws LdapException
{
next( deleteContext );

List<RegistrationEntry> selecting = getSelectingRegistrations( deleteContext.getDn(), deleteContext.getEntry() );
List<RegistrationEntry> notified = new ArrayList<>();

if ( selecting.isEmpty() )
{
return;
}

for ( final RegistrationEntry registration : selecting )
// Give the listeners a chance to read the entry before it gets deleted
for ( RegistrationEntry registration : selecting )
{
if ( EventType.isDelete( registration.getCriteria().getEventMask() ) )
{
fire( deleteContext, EventType.DELETE, registration.getListener() );
registration.getListener().beforeEntryDeleted( deleteContext );
notified.add( registration );
}
}

next( deleteContext );

for ( final RegistrationEntry registration : notified )
{
fire( deleteContext, EventType.DELETE, registration.getListener() );
}
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,26 +20,31 @@
package org.apache.directory.server.ldap.handlers;


import java.util.Collections;
import java.util.Map;
import java.util.WeakHashMap;

import org.apache.directory.api.ldap.model.constants.SchemaConstants;
import org.apache.directory.api.ldap.model.cursor.Cursor;
import org.apache.directory.api.ldap.model.entry.Entry;
import org.apache.directory.api.ldap.model.exception.LdapException;
import org.apache.directory.api.ldap.model.filter.PresenceNode;
import org.apache.directory.api.ldap.model.message.AbandonListener;
import org.apache.directory.api.ldap.model.message.AbandonableRequest;
import org.apache.directory.api.ldap.model.message.AliasDerefMode;
import org.apache.directory.api.ldap.model.message.SearchRequest;
import org.apache.directory.api.ldap.model.message.SearchResultEntry;
import org.apache.directory.api.ldap.model.message.SearchResultEntryImpl;
import org.apache.directory.api.ldap.model.message.SearchScope;
import org.apache.directory.api.ldap.model.message.controls.ChangeType;
import org.apache.directory.api.ldap.model.message.controls.EntryChange;
import org.apache.directory.api.ldap.model.message.controls.EntryChangeImpl;
import org.apache.directory.api.ldap.model.message.controls.PersistentSearch;
import org.apache.directory.api.ldap.model.schema.SchemaManager;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.directory.api.util.Strings;
import org.apache.directory.server.core.api.entry.ClonedServerEntry;
import org.apache.directory.server.core.api.entry.ServerEntryUtils;
import org.apache.directory.server.core.api.event.DirectoryListener;
import org.apache.directory.server.core.api.interceptor.context.AddOperationContext;
import org.apache.directory.server.core.api.interceptor.context.ChangeOperationContext;
import org.apache.directory.server.core.api.interceptor.context.DeleteOperationContext;
import org.apache.directory.server.core.api.interceptor.context.LookupOperationContext;
import org.apache.directory.server.core.api.interceptor.context.ModifyOperationContext;
import org.apache.directory.server.core.api.interceptor.context.MoveAndRenameOperationContext;
import org.apache.directory.server.core.api.interceptor.context.MoveOperationContext;
Expand All @@ -59,6 +64,11 @@
* cancels it. Hence time and size limits in normal search operations do not apply
* here.
*
* The changed entries are never sent as they are found in the operation context:
* each of them is read again through the session of the client which created the
* persistent search, so that the authentication and authorization checks of a
* normal search are applied before anything is sent back.
*
* @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
*/
public class PersistentSearchListener implements DirectoryListener, AbandonListener
Expand All @@ -68,18 +78,19 @@ public class PersistentSearchListener implements DirectoryListener, AbandonListe
final SearchRequest req;
final PersistentSearch psearchControl;

private LookupOperationContext filterCtx;
private SchemaManager schemaManager;
/** The attributes requested by the client */
private final String[] attributes;

/** The entries about to be deleted, as visible by the client */
private final Map<DeleteOperationContext, Entry> deletedEntries = Collections.synchronizedMap( new WeakHashMap<>() );

public PersistentSearchListener( LdapSession session, SearchRequest req )
{
this.session = session;
this.req = req;
req.addAbandonListener( this );
this.psearchControl = ( PersistentSearch ) req.getControls().get( PersistentSearch.OID );

filterCtx = new LookupOperationContext( session.getCoreSession(), req.getAttributes().toArray( Strings.EMPTY_STRING_ARRAY ) );
schemaManager = session.getCoreSession().getDirectoryService().getSchemaManager();
attributes = req.getAttributes().toArray( Strings.EMPTY_STRING_ARRAY );
}


Expand Down Expand Up @@ -145,35 +156,62 @@ public void entryAdded( AddOperationContext addContext )
return;
}

Entry entry = getVisibleEntry( addContext.getDn() );

if ( entry == null )
{
return;
}

SearchResultEntry respEntry = new SearchResultEntryImpl( req.getMessageId() );
respEntry.setObjectName( addContext.getDn() );

// the entry needs to be cloned cause addContext.getEntry() will only contain
// the user provided values and all the operational attributes added during
// Partition.add() will be applied in the cloned entry present inside it
// if we don't clone then the attributes will not be filtered
// e.x the operational attributes will also be sent even when a user requests
// user attributes only
Entry entry = new ClonedServerEntry( addContext.getEntry() );
filterEntry( entry );
respEntry.setEntry( entry );

setECResponseControl( respEntry, addContext, ChangeType.ADD );
session.getIoSession().write( respEntry );
}


/**
* {@inheritDoc}
*
* The entry can't be read anymore once it has been deleted, so we read it now,
* and keep it until we are notified of the deletion.
*/
@Override
public void beforeEntryDeleted( DeleteOperationContext deleteContext )
{
if ( !psearchControl.isNotificationEnabled( ChangeType.DELETE ) )
{
return;
}

Entry entry = getVisibleEntry( deleteContext.getDn() );

if ( entry != null )
{
deletedEntries.put( deleteContext, entry );
}
}


public void entryDeleted( DeleteOperationContext deleteContext )
{
if ( !psearchControl.isNotificationEnabled( ChangeType.DELETE ) )
{
return;
}

Entry entry = deletedEntries.remove( deleteContext );

if ( entry == null )
{
return;
}

SearchResultEntry respEntry = new SearchResultEntryImpl( req.getMessageId() );
respEntry.setObjectName( deleteContext.getDn() );
filterEntry( deleteContext.getEntry() );
respEntry.setEntry( deleteContext.getEntry() );
respEntry.setEntry( entry );
setECResponseControl( respEntry, deleteContext, ChangeType.DELETE );
session.getIoSession().write( respEntry );
}
Expand All @@ -186,11 +224,15 @@ public void entryModified( ModifyOperationContext modifyContext )
return;
}

Entry entry = getVisibleEntry( modifyContext.getDn() );

if ( entry == null )
{
return;
}

SearchResultEntry respEntry = new SearchResultEntryImpl( req.getMessageId() );
respEntry.setObjectName( modifyContext.getDn() );

Entry entry = new ClonedServerEntry( modifyContext.getAlteredEntry() );
filterEntry( entry );
respEntry.setEntry( entry );

setECResponseControl( respEntry, modifyContext, ChangeType.MODIFY );
Expand All @@ -205,11 +247,15 @@ public void entryMoved( MoveOperationContext moveContext )
return;
}

Entry entry = getVisibleEntry( moveContext.getNewDn() );

if ( entry == null )
{
return;
}

SearchResultEntry respEntry = new SearchResultEntryImpl( req.getMessageId() );
respEntry.setObjectName( moveContext.getNewDn() );

Entry entry = new ClonedServerEntry( moveContext.getModifiedEntry() );
filterEntry( entry );
respEntry.setEntry( entry );

setECResponseControl( respEntry, moveContext, ChangeType.MODDN );
Expand All @@ -230,11 +276,15 @@ public void entryRenamed( RenameOperationContext renameContext )
return;
}

Entry entry = getVisibleEntry( renameContext.getNewDn() );

if ( entry == null )
{
return;
}

SearchResultEntry respEntry = new SearchResultEntryImpl( req.getMessageId() );
respEntry.setObjectName( renameContext.getModifiedEntry().getDn() );

Entry entry = new ClonedServerEntry( renameContext.getModifiedEntry() );
filterEntry( entry );
respEntry.setObjectName( entry.getDn() );
respEntry.setEntry( entry );

setECResponseControl( respEntry, renameContext, ChangeType.MODDN );
Expand All @@ -243,22 +293,40 @@ public void entryRenamed( RenameOperationContext renameContext )


/**
* A convenient method to filter the contents of an entry
*
* @see ServerEntryUtils#filterContents(SchemaManager, org.apache.directory.server.core.api.interceptor.context.FilteringOperationContext, Entry)
*
* @param entry
* Reads an entry the way a base level search done by the client would: the
* search goes through the client's session, so the entry is only returned if
* the client is authenticated and allowed to see it, and only with the attributes
* (and values) it is allowed to read. The userPassword attribute is also removed
* if the server is configured to hide it.
*
* @param dn The entry's Dn
* @return The entry as visible by the client, or null if it can't see it
*/
private void filterEntry( Entry entry )
private Entry getVisibleEntry( Dn dn )
{
try
try ( Cursor<Entry> cursor = session.getCoreSession().search( dn, SearchScope.OBJECT,
new PresenceNode( SchemaConstants.OBJECT_CLASS_AT ), AliasDerefMode.NEVER_DEREF_ALIASES, attributes ) )
{
ServerEntryUtils.filterContents( schemaManager, filterCtx, entry );
if ( !cursor.next() )
{
return null;
}

Entry entry = cursor.get();

if ( session.getCoreSession().getDirectoryService().isPasswordHidden() )
{
entry.removeAttributes( SchemaConstants.USER_PASSWORD_AT );
}

return entry;
}
catch ( LdapException e )
catch ( Exception e )
{
// shouldn't happen, if it does then blow up
throw new RuntimeException( e );
// The client is not allowed to see the entry, or the entry does not exist anymore
LOG.debug( "Entry {} not sent to the persistent search client: {}", dn, e.getMessage() );

return null;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import org.apache.directory.api.ldap.model.filter.ExprNode;
import org.apache.directory.api.ldap.model.filter.OrNode;
import org.apache.directory.api.ldap.model.filter.PresenceNode;
import org.apache.directory.api.ldap.model.message.AliasDerefMode;
import org.apache.directory.api.ldap.model.message.Control;
import org.apache.directory.api.ldap.model.message.LdapResult;
import org.apache.directory.api.ldap.model.message.MessageTypeEnum;
Expand Down Expand Up @@ -149,6 +150,19 @@ private void handlePersistentSearch( LdapSession session, SearchRequest req, Per
return;
}
}
else
{
// Even if only the changes are requested, the client must be allowed to
// search the base : do a base level search through the client's session,
// which will throw an exception if it isn't authenticated as required, or
// if the base does not exist
try ( Cursor<Entry> cursor = session.getCoreSession().search( req.getBase(), SearchScope.OBJECT,
new PresenceNode( SchemaConstants.OBJECT_CLASS_AT ), AliasDerefMode.NEVER_DEREF_ALIASES,
SchemaConstants.NO_ATTRIBUTE ) )
{
cursor.next();
}
}

if ( req.isAbandoned() )
{
Expand Down
Loading
Loading