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.operational;
021    
022    
023    import java.util.HashSet;
024    import java.util.Iterator;
025    import java.util.List;
026    import java.util.Set;
027    import java.util.UUID;
028    
029    import org.apache.directory.server.constants.ApacheSchemaConstants;
030    import org.apache.directory.server.constants.ServerDNConstants;
031    import org.apache.directory.server.core.DirectoryService;
032    import org.apache.directory.server.core.entry.ClonedServerEntry;
033    import org.apache.directory.server.core.filtering.EntryFilter;
034    import org.apache.directory.server.core.filtering.EntryFilteringCursor;
035    import org.apache.directory.server.core.interceptor.BaseInterceptor;
036    import org.apache.directory.server.core.interceptor.Interceptor;
037    import org.apache.directory.server.core.interceptor.NextInterceptor;
038    import org.apache.directory.server.core.interceptor.context.AddOperationContext;
039    import org.apache.directory.server.core.interceptor.context.ListOperationContext;
040    import org.apache.directory.server.core.interceptor.context.LookupOperationContext;
041    import org.apache.directory.server.core.interceptor.context.ModifyOperationContext;
042    import org.apache.directory.server.core.interceptor.context.MoveAndRenameOperationContext;
043    import org.apache.directory.server.core.interceptor.context.MoveOperationContext;
044    import org.apache.directory.server.core.interceptor.context.RenameOperationContext;
045    import org.apache.directory.server.core.interceptor.context.SearchOperationContext;
046    import org.apache.directory.server.core.interceptor.context.SearchingOperationContext;
047    import org.apache.directory.server.i18n.I18n;
048    import org.apache.directory.shared.ldap.constants.SchemaConstants;
049    import org.apache.directory.shared.ldap.entry.DefaultServerAttribute;
050    import org.apache.directory.shared.ldap.entry.DefaultServerEntry;
051    import org.apache.directory.shared.ldap.entry.EntryAttribute;
052    import org.apache.directory.shared.ldap.entry.Modification;
053    import org.apache.directory.shared.ldap.entry.ModificationOperation;
054    import org.apache.directory.shared.ldap.entry.ServerEntry;
055    import org.apache.directory.shared.ldap.entry.ServerModification;
056    import org.apache.directory.shared.ldap.entry.Value;
057    import org.apache.directory.shared.ldap.exception.LdapSchemaViolationException;
058    import org.apache.directory.shared.ldap.message.ResultCodeEnum;
059    import org.apache.directory.shared.ldap.name.AVA;
060    import org.apache.directory.shared.ldap.name.DN;
061    import org.apache.directory.shared.ldap.name.RDN;
062    import org.apache.directory.shared.ldap.schema.AttributeType;
063    import org.apache.directory.shared.ldap.schema.SchemaManager;
064    import org.apache.directory.shared.ldap.schema.UsageEnum;
065    import org.apache.directory.shared.ldap.util.DateUtils;
066    import org.slf4j.Logger;
067    import org.slf4j.LoggerFactory;
068     
069    
070    /**
071     * An {@link Interceptor} that adds or modifies the default attributes
072     * of entries. There are four default attributes for now;
073     * <tt>'creatorsName'</tt>, <tt>'createTimestamp'</tt>, <tt>'modifiersName'</tt>,
074     * and <tt>'modifyTimestamp'</tt>.
075     *
076     * @org.apache.xbean.XBean
077     *
078     * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
079     * @version $Rev: 927839 $, $Date: 2010-03-26 14:25:10 +0100 (Fri, 26 Mar 2010) $
080     */
081    public class OperationalAttributeInterceptor extends BaseInterceptor
082    {
083        /** The LoggerFactory used by this Interceptor */
084        private static Logger LOG = LoggerFactory.getLogger( OperationalAttributeInterceptor.class );
085    
086        private final EntryFilter DENORMALIZING_SEARCH_FILTER = new EntryFilter()
087        {
088            public boolean accept( SearchingOperationContext operation, ClonedServerEntry serverEntry ) 
089                throws Exception
090            {
091                if ( operation.getSearchControls().getReturningAttributes() == null )
092                {
093                    return true;
094                }
095                
096                return filterDenormalized( serverEntry );
097            }
098        };
099    
100        /**
101         * the database search result filter to register with filter service
102         */
103        private final EntryFilter SEARCH_FILTER = new EntryFilter()
104        {
105            public boolean accept( SearchingOperationContext operation, ClonedServerEntry entry )
106                throws Exception
107            {
108                return operation.getSearchControls().getReturningAttributes() != null 
109                    || filterOperationalAttributes( entry );
110            }
111        };
112    
113    
114        private DirectoryService service;
115    
116        private DN subschemaSubentryDn;
117        
118        /** The schemaManager */
119        private SchemaManager schemaManager;
120        
121        private static AttributeType CREATE_TIMESTAMP_ATTRIBUTE_TYPE;
122        private static AttributeType MODIFIERS_NAME_ATTRIBUTE_TYPE;
123        private static AttributeType MODIFY_TIMESTAMP_ATTRIBUTE_TYPE;
124    
125    
126        /**
127         * Creates the operational attribute management service interceptor.
128         */
129        public OperationalAttributeInterceptor()
130        {
131        }
132    
133    
134        public void init( DirectoryService directoryService ) throws Exception
135        {
136            service = directoryService;
137            schemaManager = directoryService.getSchemaManager();
138    
139            // stuff for dealing with subentries (garbage for now)
140            Value<?> subschemaSubentry = service.getPartitionNexus()
141                    .getRootDSE( null ).get( SchemaConstants.SUBSCHEMA_SUBENTRY_AT ).get();
142            subschemaSubentryDn = new DN( subschemaSubentry.getString() );
143            subschemaSubentryDn.normalize( schemaManager.getNormalizerMapping() );
144            
145            CREATE_TIMESTAMP_ATTRIBUTE_TYPE = schemaManager.lookupAttributeTypeRegistry( SchemaConstants.CREATE_TIMESTAMP_AT );
146            MODIFIERS_NAME_ATTRIBUTE_TYPE = schemaManager.lookupAttributeTypeRegistry( SchemaConstants.MODIFIERS_NAME_AT );
147            MODIFY_TIMESTAMP_ATTRIBUTE_TYPE = schemaManager.lookupAttributeTypeRegistry( SchemaConstants.MODIFY_TIMESTAMP_AT );
148        }
149    
150    
151        public void destroy()
152        {
153        }
154    
155    
156        /**
157         * Adds extra operational attributes to the entry before it is added.
158         * 
159         * We add those attributes :
160         * - creatorsName
161         * - createTimestamp
162         * - entryCSN
163         * - entryUUID 
164         */
165        public void add( NextInterceptor nextInterceptor, AddOperationContext opContext )
166            throws Exception
167        {
168            String principal = getPrincipal().getName();
169            
170            ServerEntry entry = opContext.getEntry();
171    
172            /*
173             * @TODO : This code was probably created while working on Mitosis. Most probably dead code. Commented. 
174             * Check JIRA DIRSERVER-1416
175            if ( opContext.getEntry().containsAttribute( CREATE_TIMESTAMP_ATTRIBUTE_TYPE ) )
176            {
177                // As we already have a CreateTimeStamp value in the context, use it, but only if
178                // the principal is admin
179                if ( opContext.getSession().getAuthenticatedPrincipal().getName().equals( 
180                    ServerDNConstants.ADMIN_SYSTEM_DN_NORMALIZED ))
181                {
182                    entry.put( SchemaConstants.CREATE_TIMESTAMP_AT, DateUtils.getGeneralizedTime() );
183                }
184                else
185                {
186                    String message = "The CreateTimeStamp attribute cannot be created by a user";
187                    LOG.error( message );
188                    throw new LdapSchemaViolationException( message, ResultCodeEnum.INSUFFICIENT_ACCESS_RIGHTS );
189                }
190            }
191            else
192            {
193                entry.put( SchemaConstants.CREATE_TIMESTAMP_AT, DateUtils.getGeneralizedTime() );
194            }
195            */
196            
197            // Add the UUID and the entryCSN. The UUID is stored as a byte[] representation of 
198            // its String value
199            // @TODO : If we are using replication, those four OAs may be already present.
200            // We have to deal with this as soon as we have the replication working again
201            
202            // Check that we don't have an entryUUID AT in the incoming entry, as it's a NO-USER-MODIFICATION AT
203            // Of course, we will allow if for replication (see above @TODO)
204            boolean isAdmin = opContext.getSession().getAuthenticatedPrincipal().getName().equals( 
205                ServerDNConstants.ADMIN_SYSTEM_DN_NORMALIZED );
206            
207            if ( entry.containsAttribute( SchemaConstants.ENTRY_UUID_AT ) )
208            {
209                if ( !isAdmin )
210                {
211                    // Wrong !
212                    String message = I18n.err( I18n.ERR_30, SchemaConstants.ENTRY_UUID_AT );
213                    LOG.error( message );
214                    throw new LdapSchemaViolationException( ResultCodeEnum.INSUFFICIENT_ACCESS_RIGHTS, message );
215                }
216            }
217            else
218            {
219                entry.put( SchemaConstants.ENTRY_UUID_AT, UUID.randomUUID().toString() );
220            }
221                
222            if ( entry.containsAttribute( SchemaConstants.ENTRY_CSN_AT ) )
223            {
224                if ( !isAdmin )
225                {
226                    // Wrong !
227                    String message =  I18n.err( I18n.ERR_30, SchemaConstants.ENTRY_CSN_AT );
228                    LOG.error( message );
229                    throw new LdapSchemaViolationException( ResultCodeEnum.INSUFFICIENT_ACCESS_RIGHTS, message );
230                }
231            }
232            else
233            {
234                entry.put( SchemaConstants.ENTRY_CSN_AT, service.getCSN().toString() );
235            }
236            
237            entry.put( SchemaConstants.CREATORS_NAME_AT, principal );
238            entry.put( SchemaConstants.CREATE_TIMESTAMP_AT, DateUtils.getGeneralizedTime() );
239            
240            nextInterceptor.add( opContext );
241        }
242    
243    
244        public void modify( NextInterceptor nextInterceptor, ModifyOperationContext opContext )
245            throws Exception
246        {
247            // We must check that the user hasn't injected either the modifiersName
248            // or the modifyTimestamp operational attributes : they are not supposed to be
249            // added at this point.
250            // If so, remove them, and if there are no more attributes, simply return.
251            // otherwise, inject those values into the list of modifications
252            List<Modification> mods = opContext.getModItems();
253            
254            for ( Modification modification: mods )
255            {
256                AttributeType attributeType = modification.getAttribute().getAttributeType();
257                
258                if ( attributeType.equals( MODIFIERS_NAME_ATTRIBUTE_TYPE ) )
259                {
260                    String message = I18n.err( I18n.ERR_31 );
261                    LOG.error( message );
262                    throw new LdapSchemaViolationException( ResultCodeEnum.INSUFFICIENT_ACCESS_RIGHTS, message );
263                }
264    
265                if ( attributeType.equals( MODIFY_TIMESTAMP_ATTRIBUTE_TYPE ) )
266                {
267                    String message = I18n.err( I18n.ERR_32 );
268                    LOG.error( message );
269                    throw new LdapSchemaViolationException( ResultCodeEnum.INSUFFICIENT_ACCESS_RIGHTS, message );
270                }
271            }
272            
273            // Inject the ModifiersName AT if it's not present
274            EntryAttribute attribute = new DefaultServerAttribute( 
275                MODIFIERS_NAME_ATTRIBUTE_TYPE, 
276                getPrincipal().getName());
277    
278            Modification modifiersName = new ServerModification( ModificationOperation.REPLACE_ATTRIBUTE, attribute );
279    
280            mods.add( modifiersName );
281            
282            // Inject the ModifyTimestamp AT if it's not present
283            attribute = new DefaultServerAttribute( 
284                MODIFY_TIMESTAMP_ATTRIBUTE_TYPE,
285                DateUtils.getGeneralizedTime() );
286            
287            Modification timestamp = new ServerModification( ModificationOperation.REPLACE_ATTRIBUTE, attribute );
288    
289            mods.add( timestamp );
290            
291            // Go down in the chain
292            nextInterceptor.modify( opContext );
293            
294            if ( opContext.getDn().getNormName().equals( subschemaSubentryDn.getNormName() ) ) 
295            {
296                return;
297            }
298    
299            // -------------------------------------------------------------------
300            // Add the operational attributes for the modifier first
301            // -------------------------------------------------------------------
302            // TODO : Why can't we add those elements on teh original modifications ???
303            // Or into the context ?
304            /*
305            List<Modification> modItemList = new ArrayList<Modification>(2);
306            
307            AttributeType modifiersNameAt = atRegistry.lookup( SchemaConstants.MODIFIERS_NAME_AT );
308            ServerAttribute attribute = new DefaultServerAttribute( 
309                modifiersNameAt, 
310                getPrincipal().getName());
311    
312            Modification modifiers = new ServerModification( ModificationOperation.REPLACE_ATTRIBUTE, attribute );
313            modItemList.add( modifiers );
314            
315            AttributeType modifyTimeStampAt = atRegistry.lookup( SchemaConstants.MODIFY_TIMESTAMP_AT );
316            attribute = new DefaultServerAttribute( 
317                modifyTimeStampAt,
318                DateUtils.getGeneralizedTime() );
319            
320            Modification timestamp = new ServerModification( ModificationOperation.REPLACE_ATTRIBUTE, attribute );
321            modItemList.add( timestamp );
322    
323            // -------------------------------------------------------------------
324            // Make the modify() call happen
325            // -------------------------------------------------------------------
326            ModifyOperationContext newModify = new ModifyOperationContext( opContext.getSession(), 
327                opContext.getDn(), modItemList );
328            newModify.setEntry( opContext.getAlteredEntry() );
329            service.getPartitionNexus().modify( newModify );
330            */
331        }
332    
333    
334        public void rename( NextInterceptor nextInterceptor, RenameOperationContext opContext )
335            throws Exception
336        {
337            nextInterceptor.rename( opContext );
338    
339            DN newDn = opContext.getNewDn();
340            
341            // add operational attributes after call in case the operation fails
342            ServerEntry serverEntry = new DefaultServerEntry( schemaManager, newDn );
343            serverEntry.put( SchemaConstants.MODIFIERS_NAME_AT, getPrincipal().getName() );
344            serverEntry.put( SchemaConstants.MODIFY_TIMESTAMP_AT, DateUtils.getGeneralizedTime() );
345    
346            List<Modification> items = ModifyOperationContext.createModItems( serverEntry, ModificationOperation.REPLACE_ATTRIBUTE );
347    
348            ModifyOperationContext newModify = new ModifyOperationContext( opContext.getSession(), newDn, items );
349            newModify.setEntry( opContext.getAlteredEntry() );
350            
351            service.getPartitionNexus().modify( newModify );
352        }
353    
354    
355        public void move( NextInterceptor nextInterceptor, MoveOperationContext opContext ) throws Exception
356        {
357            nextInterceptor.move( opContext );
358    
359            // add operational attributes after call in case the operation fails
360            ServerEntry serverEntry = new DefaultServerEntry( schemaManager, opContext.getDn() );
361            serverEntry.put( SchemaConstants.MODIFIERS_NAME_AT, getPrincipal().getName() );
362            serverEntry.put( SchemaConstants.MODIFY_TIMESTAMP_AT, DateUtils.getGeneralizedTime() );
363    
364            List<Modification> items = ModifyOperationContext.createModItems( serverEntry, ModificationOperation.REPLACE_ATTRIBUTE );
365    
366    
367            ModifyOperationContext newModify = 
368                new ModifyOperationContext( opContext.getSession(), opContext.getParent(), items );
369            
370            service.getPartitionNexus().modify( newModify );
371        }
372    
373    
374        public void moveAndRename( NextInterceptor nextInterceptor, MoveAndRenameOperationContext opContext )
375            throws Exception
376        {
377            nextInterceptor.moveAndRename( opContext );
378    
379            // add operational attributes after call in case the operation fails
380            ServerEntry serverEntry = new DefaultServerEntry( schemaManager, opContext.getDn() );
381            serverEntry.put( SchemaConstants.MODIFIERS_NAME_AT, getPrincipal().getName() );
382            serverEntry.put( SchemaConstants.MODIFY_TIMESTAMP_AT, DateUtils.getGeneralizedTime() );
383    
384            List<Modification> items = ModifyOperationContext.createModItems( serverEntry, ModificationOperation.REPLACE_ATTRIBUTE );
385    
386            ModifyOperationContext newModify = 
387                new ModifyOperationContext( opContext.getSession(), opContext.getParent(), items );
388            
389            service.getPartitionNexus().modify( newModify );
390        }
391    
392    
393        public ClonedServerEntry lookup( NextInterceptor nextInterceptor, LookupOperationContext opContext ) throws Exception
394        {
395            ClonedServerEntry result = nextInterceptor.lookup( opContext );
396            
397            if ( result == null )
398            {
399                return null;
400            }
401    
402            if ( opContext.getAttrsId() == null )
403            {
404                filterOperationalAttributes( result );
405            }
406            else if ( ( opContext.getAllOperational() == null ) || ( opContext.getAllOperational() == false ) )
407            {
408                filter( opContext, result );
409            }
410            
411            denormalizeEntryOpAttrs( result );
412            return result;
413        }
414    
415    
416        public EntryFilteringCursor list( NextInterceptor nextInterceptor, ListOperationContext opContext ) throws Exception
417        {
418            EntryFilteringCursor cursor = nextInterceptor.list( opContext );
419            cursor.addEntryFilter( SEARCH_FILTER );
420            return cursor;
421        }
422    
423    
424        public EntryFilteringCursor search( NextInterceptor nextInterceptor, SearchOperationContext opContext ) throws Exception
425        {
426            EntryFilteringCursor cursor = nextInterceptor.search( opContext );
427            
428            if ( opContext.isAllOperationalAttributes() || 
429                 ( opContext.getReturningAttributes() != null && ! opContext.getReturningAttributes().isEmpty() ) )
430            {
431                if ( service.isDenormalizeOpAttrsEnabled() )
432                {
433                    cursor.addEntryFilter( DENORMALIZING_SEARCH_FILTER );
434                }
435                    
436                return cursor;
437            }
438    
439            cursor.addEntryFilter( SEARCH_FILTER );
440            return cursor;
441        }
442    
443    
444        /**
445         * Filters out the operational attributes within a search results attributes.  The attributes are directly
446         * modified.
447         *
448         * @param attributes the resultant attributes to filter
449         * @return true always
450         * @throws Exception if there are failures in evaluation
451         */
452        private boolean filterOperationalAttributes( ServerEntry attributes ) throws Exception
453        {
454            Set<AttributeType> removedAttributes = new HashSet<AttributeType>();
455    
456            // Build a list of attributeType to remove
457            for ( AttributeType attributeType:attributes.getAttributeTypes() )
458            {
459                if ( attributeType.getUsage() != UsageEnum.USER_APPLICATIONS )
460                {
461                    removedAttributes.add( attributeType );
462                }
463            }
464            
465            // Now remove the attributes which are not USERs
466            for ( AttributeType attributeType:removedAttributes )
467            {
468                attributes.removeAttributes( attributeType );
469            }
470            
471            return true;
472        }
473    
474    
475        private void filter( LookupOperationContext lookupContext, ServerEntry entry ) throws Exception
476        {
477            DN dn = lookupContext.getDn();
478            List<String> ids = lookupContext.getAttrsId();
479            
480            // still need to protect against returning op attrs when ids is null
481            if ( ids == null || ids.isEmpty() )
482            {
483                filterOperationalAttributes( entry );
484                return;
485            }
486    
487            Set<AttributeType> attributeTypes = entry.getAttributeTypes();
488    
489            if ( dn.size() == 0 )
490            {
491                for ( AttributeType attributeType:attributeTypes )
492                {
493                    if ( !ids.contains( attributeType.getOid() ) )
494                    {
495                        entry.removeAttributes( attributeType );
496                    }
497                }
498            }
499    
500            denormalizeEntryOpAttrs( entry );
501            
502            // do nothing past here since this explicity specifies which
503            // attributes to include - backends will automatically populate
504            // with right set of attributes using ids array
505        }
506    
507        
508        public void denormalizeEntryOpAttrs( ServerEntry entry ) throws Exception
509        {
510            if ( service.isDenormalizeOpAttrsEnabled() )
511            {
512                EntryAttribute attr = entry.get( SchemaConstants.CREATORS_NAME_AT );
513    
514                if ( attr != null )
515                {
516                    DN creatorsName = new DN( attr.getString() );
517                    
518                    attr.clear();
519                    attr.add( denormalizeTypes( creatorsName ).getName() );
520                }
521                
522                attr = entry.get( SchemaConstants.MODIFIERS_NAME_AT );
523                
524                if ( attr != null )
525                {
526                    DN modifiersName = new DN( attr.getString() );
527    
528                    attr.clear();
529                    attr.add( denormalizeTypes( modifiersName ).getName() );
530                }
531    
532                attr = entry.get( ApacheSchemaConstants.SCHEMA_MODIFIERS_NAME_AT );
533                
534                if ( attr != null )
535                {
536                    DN modifiersName = new DN( attr.getString() );
537    
538                    attr.clear();
539                    attr.add( denormalizeTypes( modifiersName ).getName() );
540                }
541            }
542        }
543    
544        
545        /**
546         * Does not create a new DN but alters existing DN by using the first
547         * short name for an attributeType definition.
548         * 
549         * @param dn the normalized distinguished name
550         * @return the distinuished name denormalized
551         * @throws Exception if there are problems denormalizing
552         */
553        public DN denormalizeTypes( DN dn ) throws Exception
554        {
555            DN newDn = new DN();
556            
557            for ( int ii = 0; ii < dn.size(); ii++ )
558            {
559                RDN rdn = dn.getRdn( ii );
560                if ( rdn.size() == 0 )
561                {
562                    newDn.add( new RDN() );
563                    continue;
564                }
565                else if ( rdn.size() == 1 )
566                {
567                    String name = schemaManager.lookupAttributeTypeRegistry( rdn.getNormType() ).getName();
568                    String value = rdn.getAtav().getNormValue().getString(); 
569                    newDn.add( new RDN( name, name, value, value ) );
570                    continue;
571                }
572    
573                // below we only process multi-valued rdns
574                StringBuffer buf = new StringBuffer();
575            
576                for ( Iterator<AVA> atavs = rdn.iterator(); atavs.hasNext(); /**/ )
577                {
578                    AVA atav = atavs.next();
579                    String type = schemaManager.lookupAttributeTypeRegistry( rdn.getNormType() ).getName();
580                    buf.append( type ).append( '=' ).append( atav.getNormValue() );
581                    
582                    if ( atavs.hasNext() )
583                    {
584                        buf.append( '+' );
585                    }
586                }
587                
588                newDn.add( new RDN(buf.toString()) );
589            }
590            
591            return newDn;
592        }
593    
594    
595        private boolean filterDenormalized( ServerEntry entry ) throws Exception
596        {
597            denormalizeEntryOpAttrs( entry );
598            return true;
599        }
600    }