001    /*
002     *  Licensed to the Apache Software Foundation (ASF) under one
003     *  or more contributor license agreements.  See the NOTICE file
004     *  distributed with this work for additional information
005     *  regarding copyright ownership.  The ASF licenses this file
006     *  to you under the Apache License, Version 2.0 (the
007     *  "License"); you may not use this file except in compliance
008     *  with the License.  You may obtain a copy of the License at
009     *  
010     *    http://www.apache.org/licenses/LICENSE-2.0
011     *  
012     *  Unless required by applicable law or agreed to in writing,
013     *  software distributed under the License is distributed on an
014     *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015     *  KIND, either express or implied.  See the License for the
016     *  specific language governing permissions and limitations
017     *  under the License. 
018     *  
019     */
020    package org.apache.directory.server.core.authz;
021    
022    
023    import java.text.ParseException;
024    import java.util.ArrayList;
025    import java.util.Collection;
026    import java.util.Collections;
027    import java.util.HashSet;
028    import java.util.List;
029    import java.util.Set;
030    
031    import javax.naming.directory.SearchControls;
032    
033    import org.apache.directory.server.constants.ServerDNConstants;
034    import org.apache.directory.server.core.CoreSession;
035    import org.apache.directory.server.core.DefaultCoreSession;
036    import org.apache.directory.server.core.DirectoryService;
037    import org.apache.directory.server.core.LdapPrincipal;
038    import org.apache.directory.server.core.authz.support.ACDFEngine;
039    import org.apache.directory.server.core.entry.ClonedServerEntry;
040    import org.apache.directory.server.core.entry.ServerEntryUtils;
041    import org.apache.directory.server.core.filtering.EntryFilter;
042    import org.apache.directory.server.core.filtering.EntryFilteringCursor;
043    import org.apache.directory.server.core.interceptor.BaseInterceptor;
044    import org.apache.directory.server.core.interceptor.InterceptorChain;
045    import org.apache.directory.server.core.interceptor.NextInterceptor;
046    import org.apache.directory.server.core.interceptor.context.AddOperationContext;
047    import org.apache.directory.server.core.interceptor.context.CompareOperationContext;
048    import org.apache.directory.server.core.interceptor.context.DeleteOperationContext;
049    import org.apache.directory.server.core.interceptor.context.EntryOperationContext;
050    import org.apache.directory.server.core.interceptor.context.GetMatchedNameOperationContext;
051    import org.apache.directory.server.core.interceptor.context.ListOperationContext;
052    import org.apache.directory.server.core.interceptor.context.LookupOperationContext;
053    import org.apache.directory.server.core.interceptor.context.ModifyOperationContext;
054    import org.apache.directory.server.core.interceptor.context.MoveAndRenameOperationContext;
055    import org.apache.directory.server.core.interceptor.context.MoveOperationContext;
056    import org.apache.directory.server.core.interceptor.context.OperationContext;
057    import org.apache.directory.server.core.interceptor.context.RenameOperationContext;
058    import org.apache.directory.server.core.interceptor.context.SearchOperationContext;
059    import org.apache.directory.server.core.interceptor.context.SearchingOperationContext;
060    import org.apache.directory.server.core.partition.ByPassConstants;
061    import org.apache.directory.server.core.subtree.SubentryInterceptor;
062    import org.apache.directory.server.i18n.I18n;
063    import org.apache.directory.shared.ldap.aci.ACIItem;
064    import org.apache.directory.shared.ldap.aci.ACIItemParser;
065    import org.apache.directory.shared.ldap.aci.ACITuple;
066    import org.apache.directory.shared.ldap.aci.MicroOperation;
067    import org.apache.directory.shared.ldap.constants.AuthenticationLevel;
068    import org.apache.directory.shared.ldap.constants.SchemaConstants;
069    import org.apache.directory.shared.ldap.entry.EntryAttribute;
070    import org.apache.directory.shared.ldap.entry.Modification;
071    import org.apache.directory.shared.ldap.entry.ServerEntry;
072    import org.apache.directory.shared.ldap.entry.Value;
073    import org.apache.directory.shared.ldap.exception.LdapNoPermissionException;
074    import org.apache.directory.shared.ldap.exception.LdapOperationErrorException;
075    import org.apache.directory.shared.ldap.name.DN;
076    import org.apache.directory.shared.ldap.schema.AttributeType;
077    import org.apache.directory.shared.ldap.schema.SchemaManager;
078    import org.apache.directory.shared.ldap.schema.normalizers.ConcreteNameComponentNormalizer;
079    import org.slf4j.Logger;
080    import org.slf4j.LoggerFactory;
081    
082    
083    /**
084     * An ACI based authorization service.
085     *
086     * @org.apache.xbean.XBean
087     *
088     * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
089     * @version $Rev: 928938 $
090     */
091    public class AciAuthorizationInterceptor extends BaseInterceptor
092    {
093        /** the logger for this class */
094        private static final Logger LOG = LoggerFactory.getLogger( AciAuthorizationInterceptor.class );
095    
096        /**
097         * the multivalued op attr used to track the prescriptive access control
098         * subentries that apply to an entry.
099         */
100        private static final String AC_SUBENTRY_ATTR = "accessControlSubentries";
101    
102        private static final Collection<MicroOperation> ADD_PERMS;
103        private static final Collection<MicroOperation> READ_PERMS;
104        private static final Collection<MicroOperation> COMPARE_PERMS;
105        private static final Collection<MicroOperation> SEARCH_ENTRY_PERMS;
106        private static final Collection<MicroOperation> SEARCH_ATTRVAL_PERMS;
107        private static final Collection<MicroOperation> REMOVE_PERMS;
108        private static final Collection<MicroOperation> MATCHEDNAME_PERMS;
109        private static final Collection<MicroOperation> BROWSE_PERMS;
110        private static final Collection<MicroOperation> LOOKUP_PERMS;
111        private static final Collection<MicroOperation> REPLACE_PERMS;
112        private static final Collection<MicroOperation> RENAME_PERMS;
113        private static final Collection<MicroOperation> EXPORT_PERMS;
114        private static final Collection<MicroOperation> IMPORT_PERMS;
115        private static final Collection<MicroOperation> MOVERENAME_PERMS;
116    
117        static
118        {
119            Set<MicroOperation> set = new HashSet<MicroOperation>( 2 );
120            set.add( MicroOperation.BROWSE );
121            set.add( MicroOperation.RETURN_DN );
122            SEARCH_ENTRY_PERMS = Collections.unmodifiableCollection( set );
123    
124            set = new HashSet<MicroOperation>( 2 );
125            set.add( MicroOperation.READ );
126            set.add( MicroOperation.BROWSE );
127            LOOKUP_PERMS = Collections.unmodifiableCollection( set );
128    
129            set = new HashSet<MicroOperation>( 2 );
130            set.add( MicroOperation.ADD );
131            set.add( MicroOperation.REMOVE );
132            REPLACE_PERMS = Collections.unmodifiableCollection( set );
133    
134            set = new HashSet<MicroOperation>( 2 );
135            set.add( MicroOperation.EXPORT );
136            set.add( MicroOperation.RENAME );
137            MOVERENAME_PERMS = Collections.unmodifiableCollection( set );
138    
139            SEARCH_ATTRVAL_PERMS = Collections.singleton( MicroOperation.READ );
140            ADD_PERMS = Collections.singleton( MicroOperation.ADD );
141            READ_PERMS = Collections.singleton( MicroOperation.READ );
142            COMPARE_PERMS = Collections.singleton( MicroOperation.COMPARE );
143            REMOVE_PERMS = Collections.singleton( MicroOperation.REMOVE );
144            MATCHEDNAME_PERMS = Collections.singleton( MicroOperation.DISCLOSE_ON_ERROR );
145            BROWSE_PERMS = Collections.singleton( MicroOperation.BROWSE );
146            RENAME_PERMS = Collections.singleton( MicroOperation.RENAME );
147            EXPORT_PERMS = Collections.singleton( MicroOperation.EXPORT );
148            IMPORT_PERMS = Collections.singleton( MicroOperation.IMPORT );
149        }
150    
151        /** a tupleCache that responds to add, delete, and modify attempts */
152        private TupleCache tupleCache;
153        
154        /** a groupCache that responds to add, delete, and modify attempts */
155        private GroupCache groupCache;
156        
157        /** a normalizing ACIItem parser */
158        private ACIItemParser aciParser;
159        
160        /** use and instance of the ACDF engine */
161        private ACDFEngine engine;
162        
163        /** interceptor chain */
164        private InterceptorChain chain;
165        
166        /** Global registries */
167        private SchemaManager schemaManager;
168        
169        /** the system wide subschemaSubentryDn */
170        private String subschemaSubentryDn;
171    
172        private AttributeType objectClassType;
173        private AttributeType acSubentryType;
174    
175        private String subentryOid;
176    
177        /** A storage for the entryACI attributeType */
178        private AttributeType entryAciType;
179    
180        /** the subentry ACI attribute type */
181        private AttributeType subentryAciType;
182        
183        public static final SearchControls DEFAULT_SEARCH_CONTROLS = new SearchControls();
184    
185        /**
186         * Initializes this interceptor based service by getting a handle on the nexus, setting up
187         * the tupe and group membership caches and the ACIItem parser and the ACDF engine.
188         *
189         * @param directoryService the directory service core
190         * @throws Exception if there are problems during initialization
191         */
192        public void init( DirectoryService directoryService ) throws Exception
193        {
194            super.init( directoryService );
195    
196            DN adminDn = new DN( ServerDNConstants.ADMIN_SYSTEM_DN_NORMALIZED );
197            adminDn.normalize( directoryService.getSchemaManager().getNormalizerMapping() );
198            CoreSession adminSession = new DefaultCoreSession( 
199                new LdapPrincipal( adminDn, AuthenticationLevel.STRONG ), directoryService );
200    
201            tupleCache = new TupleCache( adminSession );
202            groupCache = new GroupCache( adminSession );
203            schemaManager = directoryService.getSchemaManager();
204            //ocRegistry = registries.getObjectClassRegistry();
205            
206            // look up some constant information
207            String objectClassOid = schemaManager.getAttributeTypeRegistry().getOidByName( SchemaConstants.OBJECT_CLASS_AT );
208            subentryOid = schemaManager.getObjectClassRegistry().getOidByName( SchemaConstants.SUBENTRY_OC );
209            String acSubentryOid = schemaManager.getAttributeTypeRegistry().getOidByName( AC_SUBENTRY_ATTR );
210            objectClassType = schemaManager.lookupAttributeTypeRegistry( objectClassOid );
211            acSubentryType = schemaManager.lookupAttributeTypeRegistry( acSubentryOid );
212            entryAciType = schemaManager.lookupAttributeTypeRegistry( SchemaConstants.ENTRY_ACI_AT_OID ); 
213            subentryAciType = schemaManager.lookupAttributeTypeRegistry( SchemaConstants.SUBENTRY_ACI_AT_OID );
214            
215            aciParser = new ACIItemParser( new ConcreteNameComponentNormalizer( schemaManager ), schemaManager.getNormalizerMapping() );
216            engine = new ACDFEngine( schemaManager.getGlobalOidRegistry(), schemaManager );
217            chain = directoryService.getInterceptorChain();
218    
219            // stuff for dealing with subentries (garbage for now)
220            Value<?> subschemaSubentry = 
221                directoryService.getPartitionNexus().getRootDSE( null ).
222                    get( SchemaConstants.SUBSCHEMA_SUBENTRY_AT ).get();
223            DN subschemaSubentryDnName = new DN( subschemaSubentry.getString() );
224            subschemaSubentryDnName.normalize( schemaManager.getNormalizerMapping() );
225            subschemaSubentryDn = subschemaSubentryDnName.getNormName();
226        }
227    
228    
229        private void protectCriticalEntries( DN dn ) throws Exception
230        {
231            DN principalDn = getPrincipal().getClonedName();
232    
233            if ( dn.isEmpty() )
234            {
235                String msg = I18n.err( I18n.ERR_8 );
236                LOG.error( msg );
237                throw new LdapNoPermissionException( msg );
238            }
239    
240            if ( isTheAdministrator( dn ) )
241            {
242                String msg = I18n.err( I18n.ERR_9, principalDn.getName(), dn.getName() );
243                LOG.error( msg );
244                throw new LdapNoPermissionException( msg );
245            }
246        }
247    
248    
249        /**
250         * Adds perscriptiveACI tuples to a collection of tuples by accessing the
251         * tupleCache.  The tuple cache is accessed for each A/C subentry
252         * associated with the protected entry.  Note that subentries are handled
253         * differently: their parent, the administrative entry is accessed to
254         * determine the perscriptiveACIs effecting the AP and hence the subentry
255         * which is considered to be in the same context.
256         *
257         * @param tuples the collection of tuples to add to
258         * @param dn the normalized distinguished name of the protected entry
259         * @param entry the target entry that access to is being controled
260         * @throws Exception if there are problems accessing attribute values
261         * @param proxy the partition nexus proxy object
262         */
263        private void addPerscriptiveAciTuples( OperationContext opContext, Collection<ACITuple> tuples, DN dn,
264            ServerEntry entry ) throws Exception
265        {
266            EntryAttribute oc = null;
267            
268            if ( entry instanceof ClonedServerEntry )
269            {
270                oc = ((ClonedServerEntry)entry).getOriginalEntry().get( objectClassType );
271            }
272            else
273            {
274                oc = entry.get( objectClassType );
275            }
276            
277            /*
278             * If the protected entry is a subentry, then the entry being evaluated
279             * for perscriptiveACIs is in fact the administrative entry.  By
280             * substituting the administrative entry for the actual subentry the
281             * code below this "if" statement correctly evaluates the effects of
282             * perscriptiveACI on the subentry.  Basically subentries are considered
283             * to be in the same naming context as their access point so the subentries
284             * effecting their parent entry applies to them as well.
285             */
286            if ( oc.contains( SchemaConstants.SUBENTRY_OC ) || oc.contains( subentryOid ) )
287            {
288                DN parentDn = ( DN ) dn.clone();
289                parentDn.remove( dn.size() - 1 );
290                entry = opContext.lookup( parentDn, ByPassConstants.LOOKUP_BYPASS );
291            }
292    
293            EntryAttribute subentries = entry.get( acSubentryType );
294            
295            if ( subentries == null )
296            {
297                return;
298            }
299            
300            for ( Value<?> value:subentries )
301            {
302                String subentryDn = value.getString();
303                tuples.addAll( tupleCache.getACITuples( subentryDn ) );
304            }
305        }
306    
307    
308        /**
309         * Adds the set of entryACI tuples to a collection of tuples.  The entryACI
310         * is parsed and tuples are generated on they fly then added to the collection.
311         *
312         * @param tuples the collection of tuples to add to
313         * @param entry the target entry that access to is being regulated
314         * @throws Exception if there are problems accessing attribute values
315         */
316        private void addEntryAciTuples( Collection<ACITuple> tuples, ServerEntry entry ) throws Exception
317        {
318            EntryAttribute entryAci = entry.get( entryAciType );
319            
320            if ( entryAci == null )
321            {
322                return;
323            }
324    
325            for ( Value<?> value:entryAci )
326            {
327                String aciString = value.getString();
328                ACIItem item;
329    
330                try
331                {
332                    item = aciParser.parse( aciString );
333                }
334                catch ( ParseException e )
335                {
336                    String msg = I18n.err( I18n.ERR_10, aciString );
337                    LOG.error( msg, e );
338                    throw new LdapOperationErrorException( msg );
339                }
340    
341                tuples.addAll( item.toTuples() );
342            }
343        }
344    
345    
346        /**
347         * Adds the set of subentryACI tuples to a collection of tuples.  The subentryACI
348         * is parsed and tuples are generated on the fly then added to the collection.
349         *
350         * @param tuples the collection of tuples to add to
351         * @param dn the normalized distinguished name of the protected entry
352         * @param entry the target entry that access to is being regulated
353         * @throws Exception if there are problems accessing attribute values
354         * @param proxy the partition nexus proxy object
355         */
356        private void addSubentryAciTuples( OperationContext opContext, Collection<ACITuple> tuples, DN dn, ServerEntry entry )
357            throws Exception
358        {
359            // only perform this for subentries
360            if ( !entry.contains( SchemaConstants.OBJECT_CLASS_AT, SchemaConstants.SUBENTRY_OC ) )
361            {
362                return;
363            }
364    
365            // get the parent or administrative entry for this subentry since it
366            // will contain the subentryACI attributes that effect subentries
367            DN parentDn = ( DN ) dn.clone();
368            parentDn.remove( dn.size() - 1 );
369            ServerEntry administrativeEntry = opContext.lookup( parentDn, ByPassConstants.LOOKUP_BYPASS ).getOriginalEntry();
370            
371            EntryAttribute subentryAci = administrativeEntry.get( subentryAciType );
372    
373            if ( subentryAci == null )
374            {
375                return;
376            }
377    
378            for ( Value<?> value:subentryAci )
379            {
380                String aciString = value.getString();
381                ACIItem item;
382    
383                try
384                {
385                    item = aciParser.parse( aciString );
386                }
387                catch ( ParseException e )
388                {
389                    String msg = I18n.err( I18n.ERR_11, aciString );
390                    LOG.error( msg, e );
391                    throw new LdapOperationErrorException( msg );
392                }
393    
394                tuples.addAll( item.toTuples() );
395            }
396        }
397    
398    
399        /* -------------------------------------------------------------------------------
400         * Within every access controled interceptor method we must retrieve the ACITuple
401         * set for all the perscriptiveACIs that apply to the candidate, the target entry
402         * operated upon.  This ACITuple set is gotten from the TupleCache by looking up
403         * the subentries referenced by the accessControlSubentries operational attribute
404         * within the target entry.
405         *
406         * Then the entry is inspected for an entryACI.  This is not done for the add op
407         * since it could introduce a security breech.  So for non-add ops if present a
408         * set of ACITuples are generated for all the entryACIs within the entry.  This
409         * set is combined with the ACITuples cached for the perscriptiveACI affecting
410         * the target entry.  If the entry is a subentry the ACIs are also processed for
411         * the subentry to generate more ACITuples.  This subentry TupleACI set is joined
412         * with the entry and perscriptive ACI.
413         *
414         * The union of ACITuples are fed into the engine along with other parameters
415         * to decide whether a permission is granted or rejected for the specific
416         * operation.
417         * -------------------------------------------------------------------------------
418         */
419    
420        public void add( NextInterceptor next, AddOperationContext addContext ) throws Exception
421        {
422            // Access the principal requesting the operation, and bypass checks if it is the admin
423            LdapPrincipal principal = addContext.getSession().getEffectivePrincipal();
424            DN principalDn = principal.getClonedName();
425            
426            ServerEntry serverEntry = addContext.getEntry(); 
427            //Attributes entry = ServerEntryUtils.toAttributesImpl( serverEntry );
428    
429            DN name = addContext.getDn();
430    
431            // bypass authz code if we are disabled
432            if ( !addContext.getSession().getDirectoryService().isAccessControlEnabled() )
433            {
434                next.add( addContext );
435                return;
436            }
437    
438            // bypass authz code but manage caches if operation is performed by the admin
439            if ( isPrincipalAnAdministrator( principalDn ) )
440            {
441                next.add( addContext );
442                tupleCache.subentryAdded( name, serverEntry );
443                groupCache.groupAdded( name, serverEntry );
444                return;
445            }
446    
447            // perform checks below here for all non-admin users
448            SubentryInterceptor subentryInterceptor = ( SubentryInterceptor ) chain.get( SubentryInterceptor.class.getName() );
449            ServerEntry subentryAttrs = subentryInterceptor.getSubentryAttributes( name, serverEntry );
450            
451            for ( EntryAttribute attribute:serverEntry )
452            {
453                subentryAttrs.put( attribute );
454            }
455    
456            // Assemble all the information required to make an access control decision
457            Set<DN> userGroups = groupCache.getGroups( principalDn.getNormName() );
458            Collection<ACITuple> tuples = new HashSet<ACITuple>();
459    
460            // Build the total collection of tuples to be considered for add rights
461            // NOTE: entryACI are NOT considered in adds (it would be a security breech)
462            addPerscriptiveAciTuples( addContext, tuples, name, subentryAttrs );
463            addSubentryAciTuples( addContext, tuples, name, subentryAttrs );
464    
465            // check if entry scope permission is granted
466            engine.checkPermission( schemaManager, addContext, userGroups, principalDn, principal.getAuthenticationLevel(), name, null, null,
467                ADD_PERMS, tuples, subentryAttrs, null );
468    
469            // now we must check if attribute type and value scope permission is granted
470            for ( EntryAttribute attribute:serverEntry )
471            {
472                for ( Value<?> value:attribute )
473                {
474                    engine.checkPermission( schemaManager, addContext, userGroups, principalDn, 
475                        principal.getAuthenticationLevel(), name, attribute.getUpId(), value, 
476                        ADD_PERMS, tuples, serverEntry, null );
477                }
478            }
479    
480            // if we've gotten this far then access has been granted
481            next.add( addContext );
482    
483            // if the entry added is a subentry or a groupOf[Unique]Names we must
484            // update the ACITuple cache and the groups cache to keep them in sync
485            tupleCache.subentryAdded( name, serverEntry );
486            groupCache.groupAdded( name, serverEntry );
487        }
488    
489    
490        private boolean isTheAdministrator( DN normalizedDn )
491        {
492            return normalizedDn.getNormName().equals( ServerDNConstants.ADMIN_SYSTEM_DN_NORMALIZED );
493        }
494    
495    
496        public void delete( NextInterceptor next, DeleteOperationContext deleteContext ) throws Exception
497        {
498            DN name = deleteContext.getDn();
499            
500            LdapPrincipal principal = deleteContext.getSession().getEffectivePrincipal();
501            DN principalDn = principal.getClonedName();
502    
503            // bypass authz code if we are disabled
504            if ( ! deleteContext.getSession().getDirectoryService().isAccessControlEnabled() )
505            {
506                next.delete( deleteContext );
507                return;
508            }
509    
510            ClonedServerEntry entry = deleteContext.lookup( name, ByPassConstants.LOOKUP_BYPASS );
511    
512            protectCriticalEntries( name );
513    
514            // bypass authz code but manage caches if operation is performed by the admin
515            if ( isPrincipalAnAdministrator( principalDn ) )
516            {
517                next.delete( deleteContext );
518                tupleCache.subentryDeleted( name, entry );
519                groupCache.groupDeleted( name, entry );
520                return;
521            }
522    
523            Set<DN> userGroups = groupCache.getGroups( principalDn.getNormName() );
524            Collection<ACITuple> tuples = new HashSet<ACITuple>();
525            addPerscriptiveAciTuples( deleteContext, tuples, name, entry.getOriginalEntry() );
526            addEntryAciTuples( tuples, entry );
527            addSubentryAciTuples( deleteContext, tuples, name, entry );
528    
529            engine.checkPermission( schemaManager, deleteContext, userGroups, principalDn, 
530                principal.getAuthenticationLevel(), name, null, null, REMOVE_PERMS, tuples, entry, null );
531    
532            next.delete( deleteContext );
533            tupleCache.subentryDeleted( name, entry );
534            groupCache.groupDeleted( name, entry );
535        }
536    
537    
538        public void modify( NextInterceptor next, ModifyOperationContext opContext ) throws Exception
539        {
540            DN name = opContext.getDn();
541    
542            // Access the principal requesting the operation, and bypass checks if it is the admin
543            ClonedServerEntry entry = opContext.lookup( name, ByPassConstants.LOOKUP_BYPASS );
544            
545            LdapPrincipal principal = opContext.getSession().getEffectivePrincipal();
546            DN principalDn = principal.getClonedName();
547    
548            // bypass authz code if we are disabled
549            if ( !opContext.getSession().getDirectoryService().isAccessControlEnabled() )
550            {
551                next.modify( opContext );
552                return;
553            }
554    
555            List<Modification> mods = opContext.getModItems();
556    
557            // bypass authz code but manage caches if operation is performed by the admin
558            if ( isPrincipalAnAdministrator( principalDn ) )
559            {
560                next.modify( opContext );
561                /**
562                 * @TODO: A virtual entry can be created here for not hitting the backend again.
563                 */
564                ServerEntry modifiedEntry = opContext.lookup( name, ByPassConstants.LOOKUP_BYPASS );
565                tupleCache.subentryModified( name, mods, modifiedEntry );
566                groupCache.groupModified( name, mods, entry, schemaManager );
567                return;
568            }
569    
570            Set<DN> userGroups = groupCache.getGroups( principalDn.getName() );
571            Collection<ACITuple> tuples = new HashSet<ACITuple>();
572            addPerscriptiveAciTuples( opContext, tuples, name, entry.getOriginalEntry() );
573            addEntryAciTuples( tuples, entry );
574            addSubentryAciTuples( opContext, tuples, name, entry );
575    
576            engine.checkPermission( schemaManager, opContext, userGroups, principalDn, 
577                principal.getAuthenticationLevel(), name, null, null, 
578                Collections.singleton( MicroOperation.MODIFY ), tuples, entry, null );
579    
580            Collection<MicroOperation> perms = null;
581            ServerEntry entryView = ( ServerEntry ) entry.clone();
582            
583            for ( Modification mod : mods )
584            {
585                EntryAttribute attr = mod.getAttribute();
586    
587                switch ( mod.getOperation() )
588                {
589                    case ADD_ATTRIBUTE :
590                        perms = ADD_PERMS;
591                    
592                        // If the attribute is being created with an initial value ...
593                        if ( entry.get( attr.getId() ) == null )
594                        {
595                            // ... we also need to check if adding the attribute is permitted
596                            engine.checkPermission( schemaManager, opContext, userGroups, principalDn, principal.getAuthenticationLevel(), name,
597                                    attr.getId(), null, perms, tuples, entry, null );
598                        }
599                        
600                        break;
601    
602                    case REMOVE_ATTRIBUTE :
603                        perms = REMOVE_PERMS;
604                        EntryAttribute entryAttr = entry.get( attr.getId() );
605    
606                        if ( entryAttr != null )
607                        {
608                            // If there is only one value remaining in the attribute ...
609                            if ( entryAttr.size() == 1 )
610                            {
611                                // ... we also need to check if removing the attribute at all is permitted
612                                engine.checkPermission( schemaManager, opContext, userGroups, principalDn, 
613                                    principal.getAuthenticationLevel(), name, attr.getId(), 
614                                    null, perms, tuples, entry, null );
615                            }
616                        }
617                        
618                        break;
619    
620                    case REPLACE_ATTRIBUTE :
621                        perms = REPLACE_PERMS;
622                        break;
623                }
624    
625                /**
626                 * Update the entry view as the current modification is applied to the original entry.
627                 * This is especially required for handling the MaxValueCount protected item. Number of
628                 * values for an attribute after a modification should be known in advance in order to
629                 * check permissions for MaxValueCount protected item. So during addition of the first
630                 * value of an attribute it can be rejected if the permission denied due the the
631                 * MaxValueCount protected item. This is not the perfect implementation as required by
632                 * the specification because the system should reject the addition exactly on the right
633                 * value of the attribute. However as we do not have that much granularity in our
634                 * implementation (we consider an Attribute Addition itself a Micro Operation,
635                 * not the individual Value Additions) we just handle this when the first value of an
636                 * attribute is being checked for relevant permissions below. 
637                 */
638                entryView = ServerEntryUtils.getTargetEntry( mod, entryView, schemaManager );
639                
640                for ( Value<?> value:attr )
641                {                
642                    engine.checkPermission( schemaManager, opContext, userGroups, principalDn, 
643                        principal.getAuthenticationLevel(), name, attr.getId(), value, 
644                        perms, tuples, entry, entryView );
645                }
646            }
647    
648            
649    
650            next.modify( opContext );
651            /**
652             * @TODO: A virtual entry can be created here for not hitting the backend again.
653             */
654            ServerEntry modifiedEntry = opContext.lookup( name, ByPassConstants.LOOKUP_BYPASS );
655            tupleCache.subentryModified( name, mods, modifiedEntry );
656            groupCache.groupModified( name, mods, entry, schemaManager );
657        }
658    
659        
660        public boolean hasEntry( NextInterceptor next, EntryOperationContext entryContext ) throws Exception
661        {
662            DN name = entryContext.getDn();
663            
664            if ( ! entryContext.getSession().getDirectoryService().isAccessControlEnabled() )
665            {
666                return name.size() == 0 || next.hasEntry( entryContext );
667            }
668            
669            boolean answer = next.hasEntry( entryContext );
670    
671            // no checks on the RootDSE
672            if ( name.size() == 0 )
673            {
674                // No need to go down to the stack, if the dn is empty 
675                // It's the rootDSE, and it exists ! 
676                return answer;
677            }
678            
679            // TODO - eventually replace this with a check on session.isAnAdministrator()
680            LdapPrincipal principal = entryContext.getSession().getEffectivePrincipal();
681            DN principalDn = principal.getClonedName();
682            if ( isPrincipalAnAdministrator( principalDn ) )
683            {
684                return answer;
685            }
686    
687            ClonedServerEntry entry = entryContext.lookup( name, ByPassConstants.HAS_ENTRY_BYPASS );
688            Set<DN> userGroups = groupCache.getGroups( principalDn.getNormName() );
689            Collection<ACITuple> tuples = new HashSet<ACITuple>();
690            addPerscriptiveAciTuples( entryContext, tuples, name, entry.getOriginalEntry() );
691            addEntryAciTuples( tuples, entry.getOriginalEntry() );
692            addSubentryAciTuples( entryContext, tuples, name, entry.getOriginalEntry() );
693    
694            // check that we have browse access to the entry
695            engine.checkPermission( schemaManager, entryContext, userGroups, principalDn, 
696                principal.getAuthenticationLevel(), name, null, null,
697                BROWSE_PERMS, tuples, entry.getOriginalEntry(), null );
698    
699            return next.hasEntry( entryContext );
700        }
701    
702    
703        /**
704         * Checks if the READ permissions exist to the entry and to each attribute type and
705         * value.
706         *
707         * @todo not sure if we should hide attribute types/values or throw an exception
708         * instead.  I think we're going to have to use a filter to restrict the return
709         * of attribute types and values instead of throwing an exception.  Lack of read
710         * perms to attributes and their values results in their removal when returning
711         * the entry.
712         *
713         * @param principal the user associated with the call
714         * @param dn the name of the entry being looked up
715         * @param entry the raw entry pulled from the nexus
716         * @throws Exception if undlying access to the DIT fails
717         */
718        private void checkLookupAccess( LookupOperationContext lookupContext, ServerEntry entry ) throws Exception
719        {
720            // no permissions checks on the RootDSE
721            if ( lookupContext.getDn().getNormName().trim().equals( "" ) )
722            {
723                return;
724            }
725    
726            LdapPrincipal principal = lookupContext.getSession().getEffectivePrincipal();
727            DN userName = principal.getClonedName();
728            Set<DN> userGroups = groupCache.getGroups( userName.getNormName() );
729            Collection<ACITuple> tuples = new HashSet<ACITuple>();
730            addPerscriptiveAciTuples( lookupContext, tuples, lookupContext.getDn(), entry );
731            addEntryAciTuples( tuples, entry );
732            addSubentryAciTuples( lookupContext, tuples, lookupContext.getDn(), entry );
733    
734            // check that we have read access to the entry
735            engine.checkPermission( schemaManager, lookupContext, userGroups, userName, principal.getAuthenticationLevel(), 
736                lookupContext.getDn(), null, null,
737                LOOKUP_PERMS, tuples, entry, null );
738    
739            // check that we have read access to every attribute type and value
740            for ( EntryAttribute attribute:entry )
741            {
742                
743                for ( Value<?> value:attribute )
744                {
745                    engine.checkPermission( 
746                        schemaManager, 
747                        lookupContext, 
748                        userGroups, 
749                        userName, 
750                        principal.getAuthenticationLevel(), 
751                        lookupContext.getDn(), 
752                        attribute.getUpId(), 
753                        value, 
754                        READ_PERMS, 
755                        tuples, 
756                        entry, 
757                        null );
758                }
759            }
760        }
761    
762    
763        public ClonedServerEntry lookup( NextInterceptor next, LookupOperationContext lookupContext ) throws Exception
764        {
765            LdapPrincipal principal = lookupContext.getSession().getEffectivePrincipal();
766            DN principalDn = principal.getClonedName();
767            
768            if ( !principalDn.isNormalized() )
769            {
770                principalDn.normalize( schemaManager.getNormalizerMapping() );
771            }
772            
773            if ( isPrincipalAnAdministrator( principalDn ) || !lookupContext.getSession().getDirectoryService().isAccessControlEnabled() )
774            {
775                return next.lookup( lookupContext );
776            }
777    
778            lookupContext.setByPassed( ByPassConstants.LOOKUP_BYPASS );
779            ServerEntry entry = lookupContext.getSession().getDirectoryService()
780                .getOperationManager().lookup( lookupContext );
781    
782            checkLookupAccess( lookupContext, entry );
783            return next.lookup( lookupContext );
784        }
785    
786        
787        public void rename( NextInterceptor next, RenameOperationContext renameContext ) throws Exception
788        {
789            DN oldName = renameContext.getDn();
790            ServerEntry originalEntry = null;
791            
792            if ( renameContext.getEntry() != null )
793            {
794                originalEntry = renameContext.getEntry().getOriginalEntry();
795            }
796            
797            LdapPrincipal principal = renameContext.getSession().getEffectivePrincipal();
798            DN principalDn = principal.getClonedName();
799            DN newName = renameContext.getNewDn();
800    
801            // bypass authz code if we are disabled
802            if ( !renameContext.getSession().getDirectoryService().isAccessControlEnabled() )
803            {
804                next.rename( renameContext );
805                return;
806            }
807    
808            protectCriticalEntries( oldName );
809    
810            // bypass authz code but manage caches if operation is performed by the admin
811            if ( isPrincipalAnAdministrator( principalDn ) )
812            {
813                next.rename( renameContext );
814                tupleCache.subentryRenamed( oldName, newName );
815                
816                // TODO : this method returns a boolean : what should we do with the result ?
817                groupCache.groupRenamed( oldName, newName );
818    
819                return;
820            }
821    
822            Set<DN> userGroups = groupCache.getGroups( principalDn.getNormName() );
823            Collection<ACITuple> tuples = new HashSet<ACITuple>();
824            addPerscriptiveAciTuples( renameContext, tuples, oldName, originalEntry );
825            addEntryAciTuples( tuples, originalEntry );
826            addSubentryAciTuples( renameContext, tuples, oldName, originalEntry );
827    
828            engine.checkPermission( schemaManager, renameContext, userGroups, principalDn, 
829                principal.getAuthenticationLevel(), oldName, null, null,
830                RENAME_PERMS, tuples, originalEntry, null );
831    
832            next.rename( renameContext );
833            tupleCache.subentryRenamed( oldName, newName );
834            groupCache.groupRenamed( oldName, newName );
835        }
836    
837    
838        public void moveAndRename( NextInterceptor next, MoveAndRenameOperationContext moveAndRenameContext )
839            throws Exception
840        {
841            DN oriChildName = moveAndRenameContext.getDn();
842            DN newParentName = moveAndRenameContext.getParent();
843    
844            ClonedServerEntry entry = moveAndRenameContext.lookup( oriChildName, ByPassConstants.LOOKUP_BYPASS );
845            
846            LdapPrincipal principal = moveAndRenameContext.getSession().getEffectivePrincipal();
847            DN principalDn = principal.getClonedName();
848            DN newName = ( DN ) newParentName.clone();
849            newName.add( moveAndRenameContext.getNewRdn().getName() );
850    
851            // bypass authz code if we are disabled
852            if ( !moveAndRenameContext.getSession().getDirectoryService().isAccessControlEnabled() )
853            {
854                next.moveAndRename( moveAndRenameContext );
855                return;
856            }
857    
858            protectCriticalEntries( oriChildName );
859    
860            // bypass authz code but manage caches if operation is performed by the admin
861            if ( isPrincipalAnAdministrator( principalDn ) )
862            {
863                next.moveAndRename( moveAndRenameContext );
864                tupleCache.subentryRenamed( oriChildName, newName );
865                groupCache.groupRenamed( oriChildName, newName );
866                return;
867            }
868    
869            Set<DN> userGroups = groupCache.getGroups( principalDn.getNormName() );
870            Collection<ACITuple> tuples = new HashSet<ACITuple>();
871            addPerscriptiveAciTuples( moveAndRenameContext, tuples, oriChildName, entry.getOriginalEntry() );
872            addEntryAciTuples( tuples, entry );
873            addSubentryAciTuples( moveAndRenameContext, tuples, oriChildName, entry );
874    
875            engine.checkPermission( schemaManager, moveAndRenameContext, userGroups, 
876                principalDn, principal.getAuthenticationLevel(), oriChildName, null,
877                null, MOVERENAME_PERMS, tuples, entry, null );
878    
879            // Get the entry again without operational attributes
880            // because access control subentry operational attributes
881            // will not be valid at the new location.
882            // This will certainly be fixed by the SubentryInterceptor,
883            // but after this service.
884            
885            ClonedServerEntry importedEntry = moveAndRenameContext.lookup( oriChildName, 
886                ByPassConstants.LOOKUP_EXCLUDING_OPR_ATTRS_BYPASS );
887            
888            // As the target entry does not exist yet and so
889            // its subentry operational attributes are not there,
890            // we need to construct an entry to represent it
891            // at least with minimal requirements which are object class
892            // and access control subentry operational attributes.
893            SubentryInterceptor subentryInterceptor = ( SubentryInterceptor ) chain.get( SubentryInterceptor.class.getName() );
894            ServerEntry subentryAttrs = subentryInterceptor.getSubentryAttributes( newName, importedEntry );
895            
896            for ( EntryAttribute attribute:importedEntry )
897            {
898                subentryAttrs.put( attribute );
899            }
900            
901            Collection<ACITuple> destTuples = new HashSet<ACITuple>();
902            // Import permission is only valid for prescriptive ACIs
903            addPerscriptiveAciTuples( moveAndRenameContext, destTuples, newName, subentryAttrs );
904            // Evaluate the target context to see whether it
905            // allows an entry named newName to be imported as a subordinate.
906            engine.checkPermission( schemaManager, moveAndRenameContext, userGroups, principalDn, 
907                principal.getAuthenticationLevel(), newName, null,
908                null, IMPORT_PERMS, destTuples, subentryAttrs, null );
909    
910    
911            next.moveAndRename( moveAndRenameContext );
912            tupleCache.subentryRenamed( oriChildName, newName );
913            groupCache.groupRenamed( oriChildName, newName );
914        }
915    
916    
917        public void move( NextInterceptor next, MoveOperationContext moveContext ) throws Exception
918        {
919            DN oriChildName = moveContext.getDn();
920            DN newParentName = moveContext.getParent();
921            
922            // Access the principal requesting the operation, and bypass checks if it is the admin
923            ClonedServerEntry entry = moveContext.lookup( oriChildName, ByPassConstants.LOOKUP_BYPASS );
924           
925            DN newName = ( DN ) newParentName.clone();
926            newName.add( oriChildName.get( oriChildName.size() - 1 ) );
927            LdapPrincipal principal = moveContext.getSession().getEffectivePrincipal();
928            DN principalDn = principal.getClonedName();
929    
930            // bypass authz code if we are disabled
931            if ( !moveContext.getSession().getDirectoryService().isAccessControlEnabled() )
932            {
933                next.move( moveContext );
934                return;
935            }
936    
937            protectCriticalEntries( oriChildName);
938    
939            // bypass authz code but manage caches if operation is performed by the admin
940            if ( isPrincipalAnAdministrator( principalDn ) )
941            {
942                next.move( moveContext );
943                tupleCache.subentryRenamed( oriChildName, newName );
944                groupCache.groupRenamed( oriChildName, newName );
945                return;
946            }
947    
948            Set<DN> userGroups = groupCache.getGroups( principalDn.getNormName() );
949            Collection<ACITuple> tuples = new HashSet<ACITuple>();
950            addPerscriptiveAciTuples( moveContext, tuples, oriChildName, entry.getOriginalEntry() );
951            addEntryAciTuples( tuples, entry );
952            addSubentryAciTuples( moveContext, tuples, oriChildName, entry );
953    
954            engine.checkPermission( schemaManager, moveContext, userGroups, principalDn, 
955                principal.getAuthenticationLevel(), oriChildName, null,
956                null, EXPORT_PERMS, tuples, entry, null );
957            
958            // Get the entry again without operational attributes
959            // because access control subentry operational attributes
960            // will not be valid at the new location.
961            // This will certainly be fixed by the SubentryInterceptor,
962            // but after this service.
963            ServerEntry importedEntry = moveContext.lookup( oriChildName, 
964                ByPassConstants.LOOKUP_EXCLUDING_OPR_ATTRS_BYPASS );
965                
966            // As the target entry does not exist yet and so
967            // its subentry operational attributes are not there,
968            // we need to construct an entry to represent it
969            // at least with minimal requirements which are object class
970            // and access control subentry operational attributes.
971            SubentryInterceptor subentryInterceptor = ( SubentryInterceptor ) 
972                chain.get( SubentryInterceptor.class.getName() );
973            ServerEntry subentryAttrs = subentryInterceptor.getSubentryAttributes( newName, importedEntry );
974            
975            for ( EntryAttribute attribute:importedEntry )
976            {
977                subentryAttrs.put( attribute );
978            }
979            
980            Collection<ACITuple> destTuples = new HashSet<ACITuple>();
981            // Import permission is only valid for prescriptive ACIs
982            addPerscriptiveAciTuples( moveContext, destTuples, newName, subentryAttrs );
983            // Evaluate the target context to see whether it
984            // allows an entry named newName to be imported as a subordinate.
985            engine.checkPermission( schemaManager, moveContext, userGroups, principalDn, 
986                principal.getAuthenticationLevel(), newName, null,
987                null, IMPORT_PERMS, destTuples, subentryAttrs, null );
988    
989            next.move( moveContext );
990            tupleCache.subentryRenamed( oriChildName, newName );
991            groupCache.groupRenamed( oriChildName, newName );
992        }
993    
994        
995        public EntryFilteringCursor list( NextInterceptor next, ListOperationContext opContext ) throws Exception
996        {
997            LdapPrincipal user = opContext.getSession().getEffectivePrincipal();
998            EntryFilteringCursor cursor = next.list( opContext );
999            
1000            if ( isPrincipalAnAdministrator( user.getClonedName() ) || !opContext.getSession().getDirectoryService().isAccessControlEnabled() )
1001            {
1002                return cursor;
1003            }
1004            
1005            AuthorizationFilter authzFilter = new AuthorizationFilter();
1006            cursor.addEntryFilter( authzFilter );
1007            return cursor;
1008        }
1009    
1010    
1011        public EntryFilteringCursor search( NextInterceptor next, SearchOperationContext opContext ) throws Exception
1012        {
1013            LdapPrincipal user = opContext.getSession().getEffectivePrincipal();
1014            DN principalDn = user.getClonedName();
1015            EntryFilteringCursor cursor = next.search( opContext );
1016    
1017            boolean isSubschemaSubentryLookup = subschemaSubentryDn.equals( opContext.getDn().getNormName() );
1018            SearchControls searchCtls = opContext.getSearchControls();
1019            boolean isRootDSELookup = opContext.getDn().size() == 0 && searchCtls.getSearchScope() == SearchControls.OBJECT_SCOPE;
1020    
1021            if ( isPrincipalAnAdministrator( principalDn ) || !opContext.getSession().getDirectoryService().isAccessControlEnabled() || isRootDSELookup || isSubschemaSubentryLookup )
1022            {
1023                return cursor;
1024            }
1025            
1026            cursor.addEntryFilter( new AuthorizationFilter() );
1027            return cursor;
1028        }
1029    
1030        
1031        public final boolean isPrincipalAnAdministrator( DN principalDn )
1032        {
1033            return groupCache.isPrincipalAnAdministrator( principalDn );
1034        }
1035        
1036    
1037        public boolean compare( NextInterceptor next, CompareOperationContext opContext ) throws Exception
1038        {
1039            DN name = opContext.getDn();
1040            String oid = opContext.getOid();
1041            Value<?> value = ( Value<?> ) opContext.getValue();
1042    
1043            ClonedServerEntry entry = opContext.lookup( name, ByPassConstants.LOOKUP_BYPASS );
1044    
1045            LdapPrincipal principal = opContext.getSession().getEffectivePrincipal();
1046            DN principalDn = principal.getClonedName();
1047    
1048            if ( isPrincipalAnAdministrator( principalDn ) || !opContext.getSession().getDirectoryService().isAccessControlEnabled() )
1049            {
1050                return next.compare( opContext );
1051            }
1052    
1053            Set<DN> userGroups = groupCache.getGroups( principalDn.getNormName() );
1054            Collection<ACITuple> tuples = new HashSet<ACITuple>();
1055            addPerscriptiveAciTuples( opContext, tuples, name, entry.getOriginalEntry() );
1056            addEntryAciTuples( tuples, entry );
1057            addSubentryAciTuples( opContext, tuples, name, entry );
1058    
1059            engine.checkPermission( schemaManager, opContext, userGroups, principalDn, 
1060                principal.getAuthenticationLevel(), name, null, null,
1061                READ_PERMS, tuples, entry, null );
1062            engine.checkPermission( schemaManager, opContext, userGroups, principalDn, 
1063                principal.getAuthenticationLevel(), name, oid, value,
1064                COMPARE_PERMS, tuples, entry, null );
1065    
1066            return next.compare( opContext );
1067        }
1068    
1069    
1070        public DN getMatchedName ( NextInterceptor next, GetMatchedNameOperationContext opContext ) throws Exception
1071        {
1072            // Access the principal requesting the operation, and bypass checks if it is the admin
1073            LdapPrincipal principal = opContext.getSession().getEffectivePrincipal();
1074            DN principalDn = principal.getClonedName();
1075            
1076            if ( isPrincipalAnAdministrator( principalDn ) || !opContext.getSession().getDirectoryService().isAccessControlEnabled() )
1077            {
1078                return next.getMatchedName( opContext );
1079            }
1080    
1081            // get the present matched name
1082            ClonedServerEntry entry;
1083            DN matched = next.getMatchedName( opContext );
1084    
1085            // check if we have disclose on error permission for the entry at the matched dn
1086            // if not remove rdn and check that until nothing is left in the name and return
1087            // that but if permission is granted then short the process and return the dn
1088            while ( matched.size() > 0 )
1089            {
1090                entry = opContext.lookup( matched, ByPassConstants.GETMATCHEDDN_BYPASS );
1091                
1092                Set<DN> userGroups = groupCache.getGroups( principalDn.getNormName() );
1093                Collection<ACITuple> tuples = new HashSet<ACITuple>();
1094                addPerscriptiveAciTuples( opContext, tuples, matched, entry.getOriginalEntry() );
1095                addEntryAciTuples( tuples, entry );
1096                addSubentryAciTuples( opContext, tuples, matched, entry );
1097    
1098                if ( engine.hasPermission( schemaManager, opContext, userGroups, principalDn, 
1099                    principal.getAuthenticationLevel(), matched, null,
1100                    null, MATCHEDNAME_PERMS, tuples, entry, null ) )
1101                {
1102                    return matched;
1103                }
1104    
1105                matched.remove( matched.size() - 1 );
1106            }
1107    
1108            return matched;
1109        }
1110    
1111    
1112        public void cacheNewGroup( DN name, ServerEntry entry ) throws Exception
1113        {
1114            groupCache.groupAdded( name, entry );
1115        }
1116    
1117    
1118        private boolean filter( OperationContext opContext, DN normName, ClonedServerEntry clonedEntry ) 
1119            throws Exception
1120        {
1121            /*
1122             * First call hasPermission() for entry level "Browse" and "ReturnDN" perm
1123             * tests.  If we hasPermission() returns false we immediately short the
1124             * process and return false.
1125             */
1126            
1127            LdapPrincipal principal = opContext.getSession().getEffectivePrincipal();
1128            DN userDn = principal.getClonedName();
1129            Set<DN> userGroups = groupCache.getGroups( userDn.getNormName() );
1130            Collection<ACITuple> tuples = new HashSet<ACITuple>();
1131            addPerscriptiveAciTuples( opContext, tuples, normName, clonedEntry.getOriginalEntry() );
1132            addEntryAciTuples( tuples, clonedEntry.getOriginalEntry() );
1133            addSubentryAciTuples( opContext, tuples, normName, clonedEntry.getOriginalEntry() );
1134    
1135            if ( !engine.hasPermission( 
1136                            schemaManager, 
1137                            opContext, 
1138                            userGroups, 
1139                            userDn, 
1140                            principal.getAuthenticationLevel(), 
1141                            normName, 
1142                            null, 
1143                            null, 
1144                            SEARCH_ENTRY_PERMS, 
1145                            tuples, 
1146                            clonedEntry.getOriginalEntry(), 
1147                            null ) )
1148            {
1149                return false;
1150            }
1151    
1152            /*
1153             * For each attribute type we check if access is allowed to the type.  If not
1154             * the attribute is yanked out of the entry to be returned.  If permission is
1155             * allowed we move on to check if the values are allowed.  Values that are
1156             * not allowed are removed from the attribute.  If the attribute has no more
1157             * values remaining then the entire attribute is removed.
1158             */
1159            List<AttributeType> attributeToRemove = new ArrayList<AttributeType>();
1160            
1161            for ( AttributeType attributeType:clonedEntry.getAttributeTypes() )
1162            {
1163                // if attribute type scope access is not allowed then remove the attribute and continue
1164                String id = attributeType.getName();
1165                EntryAttribute attr = clonedEntry.get( attributeType );
1166            
1167                if ( !engine.hasPermission( 
1168                            schemaManager, 
1169                            opContext, 
1170                            userGroups, 
1171                            userDn,
1172                            principal.getAuthenticationLevel(), 
1173                            normName, 
1174                            id, 
1175                            null, 
1176                            SEARCH_ATTRVAL_PERMS, 
1177                            tuples, 
1178                            clonedEntry, 
1179                            null ) )
1180                {
1181                    attributeToRemove.add( attributeType );
1182                    
1183                    continue;
1184                }
1185    
1186                List<Value<?>> valueToRemove = new ArrayList<Value<?>>();
1187                
1188                // attribute type scope is ok now let's determine value level scope
1189                for ( Value<?> value:attr )
1190                {
1191                    if ( !engine.hasPermission( 
1192                            schemaManager, 
1193                            opContext, 
1194                            userGroups, 
1195                            userDn, 
1196                            principal.getAuthenticationLevel(), 
1197                            normName, 
1198                            attr.getUpId(), 
1199                            value, 
1200                            SEARCH_ATTRVAL_PERMS, 
1201                            tuples,
1202                            clonedEntry, 
1203                            null ) )
1204                    {
1205                        valueToRemove.add( value );
1206                    }
1207                }
1208                
1209                for ( Value<?> value:valueToRemove )
1210                {
1211                    attr.remove( value );
1212                }
1213                
1214                if ( attr.size() == 0 )
1215                {
1216                    attributeToRemove.add( attributeType );
1217                }
1218            }
1219            
1220            for ( AttributeType attributeType:attributeToRemove )
1221            {
1222                clonedEntry.removeAttributes( attributeType );
1223            }
1224    
1225            return true;
1226        }
1227    
1228    
1229        /**
1230         * WARNING: create one of these filters fresh every time for each new search.
1231         */
1232        class AuthorizationFilter implements EntryFilter
1233        {
1234            public boolean accept( SearchingOperationContext operationContext, ClonedServerEntry entry ) 
1235                throws Exception
1236            {
1237                DN normName = entry.getDn().normalize( schemaManager.getNormalizerMapping() );
1238                return filter( operationContext, normName, entry );
1239            }
1240        }
1241    }