View Javadoc

1   /*
2    *  Licensed to the Apache Software Foundation (ASF) under one
3    *  or more contributor license agreements.  See the NOTICE file
4    *  distributed with this work for additional information
5    *  regarding copyright ownership.  The ASF licenses this file
6    *  to you under the Apache License, Version 2.0 (the
7    *  "License"); you may not use this file except in compliance
8    *  with the License.  You may obtain a copy of the License at
9    *  
10   *    http://www.apache.org/licenses/LICENSE-2.0
11   *  
12   *  Unless required by applicable law or agreed to in writing,
13   *  software distributed under the License is distributed on an
14   *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   *  KIND, either express or implied.  See the License for the
16   *  specific language governing permissions and limitations
17   *  under the License. 
18   *  
19   */
20  package org.apache.directory.server.core.exception;
21  
22  
23  import org.apache.commons.collections.map.LRUMap;
24  import org.apache.directory.server.core.DirectoryService;
25  import org.apache.directory.server.core.cursor.EmptyCursor;
26  import org.apache.directory.server.core.entry.ClonedServerEntry;
27  import org.apache.directory.server.core.entry.ServerAttribute;
28  import org.apache.directory.server.core.entry.ServerEntry;
29  import org.apache.directory.server.core.filtering.EntryFilteringCursor;
30  import org.apache.directory.server.core.filtering.BaseEntryFilteringCursor;
31  import org.apache.directory.server.core.interceptor.BaseInterceptor;
32  import org.apache.directory.server.core.interceptor.NextInterceptor;
33  import org.apache.directory.server.core.interceptor.context.AddOperationContext;
34  import org.apache.directory.server.core.interceptor.context.DeleteOperationContext;
35  import org.apache.directory.server.core.interceptor.context.EntryOperationContext;
36  import org.apache.directory.server.core.interceptor.context.GetMatchedNameOperationContext;
37  import org.apache.directory.server.core.interceptor.context.GetSuffixOperationContext;
38  import org.apache.directory.server.core.interceptor.context.ListOperationContext;
39  import org.apache.directory.server.core.interceptor.context.LookupOperationContext;
40  import org.apache.directory.server.core.interceptor.context.ModifyOperationContext;
41  import org.apache.directory.server.core.interceptor.context.MoveAndRenameOperationContext;
42  import org.apache.directory.server.core.interceptor.context.MoveOperationContext;
43  import org.apache.directory.server.core.interceptor.context.OperationContext;
44  import org.apache.directory.server.core.interceptor.context.RenameOperationContext;
45  import org.apache.directory.server.core.interceptor.context.SearchOperationContext;
46  import org.apache.directory.server.core.partition.ByPassConstants;
47  import org.apache.directory.server.core.partition.Partition;
48  import org.apache.directory.server.core.partition.PartitionNexus;
49  import org.apache.directory.shared.ldap.constants.SchemaConstants;
50  import org.apache.directory.shared.ldap.entry.EntryAttribute;
51  import org.apache.directory.shared.ldap.entry.Modification;
52  import org.apache.directory.shared.ldap.entry.ModificationOperation;
53  import org.apache.directory.shared.ldap.entry.Value;
54  import org.apache.directory.shared.ldap.exception.LdapAttributeInUseException;
55  import org.apache.directory.shared.ldap.exception.LdapContextNotEmptyException;
56  import org.apache.directory.shared.ldap.exception.LdapNameAlreadyBoundException;
57  import org.apache.directory.shared.ldap.exception.LdapNameNotFoundException;
58  import org.apache.directory.shared.ldap.exception.LdapNamingException;
59  import org.apache.directory.shared.ldap.exception.LdapOperationNotSupportedException;
60  import org.apache.directory.shared.ldap.message.ResultCodeEnum;
61  import org.apache.directory.shared.ldap.name.LdapDN;
62  import org.apache.directory.shared.ldap.schema.OidNormalizer;
63  
64  import java.util.List;
65  import java.util.Map;
66  
67  
68  /**
69   * An {@link org.apache.directory.server.core.interceptor.Interceptor} that detects any operations that breaks integrity
70   * of {@link Partition} and terminates the current invocation chain by
71   * throwing a {@link Exception}. Those operations include when an entry
72   * already exists at a DN and is added once again to the same DN.
73   *
74   * @org.apache.xbean.XBean
75   *
76   * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
77   * @version $Rev: 689396 $
78   */
79  public class ExceptionInterceptor extends BaseInterceptor
80  {
81      private PartitionNexus nexus;
82      private DirectoryService directoryService;
83      private LdapDN subschemSubentryDn;
84  
85      
86      /**
87       * The OIDs normalizer map
88       */
89      private Map<String, OidNormalizer> normalizerMap;
90      
91      /**
92       * A cache to store entries which are not aliases. 
93       * It's a speedup, we will be able to avoid backend lookups.
94       * 
95       * Note that the backend also use a cache mechanism, but for performance gain, it's good 
96       * to manage a cache here. The main problem is that when a user modify the parent, we will
97       * have to update it at three different places :
98       * - in the backend,
99       * - in the partition cache,
100      * - in this cache.
101      * 
102      * The update of the backend and partition cache is already correctly handled, so we will
103      * just have to offer an access to refresh the local cache. This should be done in 
104      * delete, modify and move operations.
105      * 
106      * We need to be sure that frequently used DNs are always in cache, and not discarded.
107      * We will use a LRU cache for this purpose. 
108      */ 
109     private final LRUMap notAliasCache = new LRUMap( DEFAULT_CACHE_SIZE );
110 
111     /** Declare a default for this cache. 100 entries seems to be enough */
112     private static final int DEFAULT_CACHE_SIZE = 100;
113 
114     
115     /**
116      * Creates an interceptor that is also the exception handling service.
117      */
118     public ExceptionInterceptor()
119     {
120     }
121 
122 
123     public void init( DirectoryService directoryService ) throws Exception
124     {
125         this.directoryService = directoryService;
126         nexus = directoryService.getPartitionNexus();
127         normalizerMap = directoryService.getRegistries().getAttributeTypeRegistry().getNormalizerMapping();
128         Value<?> attr = nexus.getRootDSE( null ).get( SchemaConstants.SUBSCHEMA_SUBENTRY_AT ).get();
129         subschemSubentryDn = new LdapDN( ( String ) attr.get() );
130         subschemSubentryDn.normalize( normalizerMap );
131     }
132 
133 
134     public void destroy()
135     {
136     }
137 
138     /**
139      * In the pre-invocation state this interceptor method checks to see if the entry to be added already exists.  If it
140      * does an exception is raised.
141      */
142     public void add( NextInterceptor nextInterceptor, AddOperationContext opContext )
143         throws Exception
144     {
145         LdapDN name = opContext.getDn();
146         
147         if ( subschemSubentryDn.getNormName().equals( name.getNormName() ) )
148         {
149             throw new LdapNameAlreadyBoundException( 
150                 "The global schema subentry cannot be added since it exists by default." );
151         }
152         
153         // check if the entry already exists
154         if ( nextInterceptor.hasEntry( new EntryOperationContext( opContext.getSession(), name ) ) )
155         {
156             LdapNameAlreadyBoundException ne = new LdapNameAlreadyBoundException( name.getUpName() + " already exists!" );
157             ne.setResolvedName( new LdapDN( name.getUpName() ) );
158             throw ne;
159         }
160         
161         LdapDN suffix = nexus.getSuffix( new GetSuffixOperationContext( this.directoryService.getAdminSession(), 
162             name ) );
163         
164         // we're adding the suffix entry so just ignore stuff to mess with the parent
165         if ( suffix.getNormName().equals( name.getNormName() ) )
166         {
167             nextInterceptor.add( opContext );
168             return;
169         }
170         
171         LdapDN parentDn = ( LdapDN ) name.clone();
172         parentDn.remove( name.size() - 1 );
173         
174         // check if we're trying to add to a parent that is an alias
175         boolean notAnAlias;
176         
177         synchronized( notAliasCache )
178         {
179             notAnAlias = notAliasCache.containsKey( parentDn.getNormName() );
180         }
181         
182         if ( ! notAnAlias )
183         {
184             // We don't know if the parent is an alias or not, so we will launch a 
185             // lookup, and update the cache if it's not an alias
186             ClonedServerEntry attrs;
187             
188             try
189             {
190                 attrs = opContext.lookup( parentDn, ByPassConstants.LOOKUP_BYPASS );
191             }
192             catch ( Exception e )
193             {
194                 LdapNameNotFoundException e2 = new LdapNameNotFoundException( "Parent " + parentDn.getUpName() 
195                     + " not found" );
196                 e2.setResolvedName( new LdapDN( nexus.getMatchedName( 
197                     new GetMatchedNameOperationContext( opContext.getSession(), parentDn ) ).getUpName() ) );
198                 throw e2;
199             }
200             
201             EntryAttribute objectClass = attrs.getOriginalEntry().get( SchemaConstants.OBJECT_CLASS_AT );
202             
203             if ( objectClass.contains( SchemaConstants.ALIAS_OC ) )
204             {
205                 String msg = "Attempt to add entry to alias '" + name.getUpName() + "' not allowed.";
206                 ResultCodeEnum rc = ResultCodeEnum.ALIAS_PROBLEM;
207                 LdapNamingException e = new LdapNamingException( msg, rc );
208                 e.setResolvedName( new LdapDN( parentDn.getUpName() ) );
209                 throw e;
210             }
211             else
212             {
213                 synchronized ( notAliasCache )
214                 {
215                     notAliasCache.put( parentDn.getNormName(), parentDn );
216                 }
217             }
218         }
219 
220         nextInterceptor.add( opContext );
221     }
222 
223 
224     /**
225      * Checks to make sure the entry being deleted exists, and has no children, otherwise throws the appropriate
226      * LdapException.
227      */
228     public void delete( NextInterceptor nextInterceptor, DeleteOperationContext opContext ) throws Exception
229     {
230         LdapDN name = opContext.getDn();
231         
232         if ( name.getNormName().equalsIgnoreCase( subschemSubentryDn.getNormName() ) )
233         {
234             throw new LdapOperationNotSupportedException( 
235                 "Can not allow the deletion of the subschemaSubentry (" + 
236                 subschemSubentryDn + ") for the global schema.",
237                 ResultCodeEnum.UNWILLING_TO_PERFORM );
238         }
239         
240         // check if entry to delete exists
241         String msg = "Attempt to delete non-existant entry: ";
242         assertHasEntry( nextInterceptor, opContext, msg, name );
243 
244         // check if entry to delete has children (only leaves can be deleted)
245         boolean hasChildren = false;
246         EntryFilteringCursor list = nextInterceptor.list( new ListOperationContext( opContext.getSession(), name ) );
247         
248         if ( list.next() )
249         {
250             hasChildren = true;
251         }
252 
253         list.close();
254         
255         if ( hasChildren )
256         {
257             LdapContextNotEmptyException e = new LdapContextNotEmptyException();
258             e.setResolvedName( new LdapDN( name.getUpName() ) );
259             throw e;
260         }
261 
262         synchronized( notAliasCache )
263         {
264             if ( notAliasCache.containsKey( name.getNormName() ) )
265             {
266                 notAliasCache.remove( name.getNormName() );
267             }
268         }
269         
270         nextInterceptor.delete( opContext );
271     }
272 
273 
274     /**
275      * Checks to see the base being searched exists, otherwise throws the appropriate LdapException.
276      */
277     public EntryFilteringCursor list( NextInterceptor nextInterceptor, ListOperationContext opContext ) throws Exception
278     {
279         if ( opContext.getDn().getNormName().equals( subschemSubentryDn.getNormName() ) )
280         {
281             // there is nothing under the schema subentry
282             return new BaseEntryFilteringCursor( new EmptyCursor<ServerEntry>(), opContext );
283         }
284         
285         // check if entry to search exists
286         String msg = "Attempt to search under non-existant entry: ";
287         assertHasEntry( nextInterceptor, opContext, msg, opContext.getDn() );
288 
289         return nextInterceptor.list( opContext );
290     }
291 
292 
293     /**
294      * Checks to see the base being searched exists, otherwise throws the appropriate LdapException.
295      */
296     public ClonedServerEntry lookup( NextInterceptor nextInterceptor, LookupOperationContext opContext ) throws Exception
297     {
298         if ( opContext.getDn().getNormName().equals( subschemSubentryDn.getNormName() ) )
299         {
300             return nexus.getRootDSE( null );
301         }
302         
303         // check if entry to lookup exists
304         String msg = "Attempt to lookup non-existant entry: ";
305         assertHasEntry( nextInterceptor, opContext, msg, opContext.getDn() );
306 
307         return nextInterceptor.lookup( opContext );
308     }
309 
310 
311     /**
312      * Checks to see the entry being modified exists, otherwise throws the appropriate LdapException.
313      */
314     public void modify( NextInterceptor nextInterceptor, ModifyOperationContext opContext )
315         throws Exception
316     {
317         // check if entry to modify exists
318         String msg = "Attempt to modify non-existant entry: ";
319 
320         // handle operations against the schema subentry in the schema service
321         // and never try to look it up in the nexus below
322         if ( opContext.getDn().getNormName().equalsIgnoreCase( subschemSubentryDn.getNormName() ) )
323         {
324             nextInterceptor.modify( opContext );
325             return;
326         }
327         
328         assertHasEntry( nextInterceptor, opContext, msg, opContext.getDn() );
329 
330         ServerEntry entry = opContext.lookup( opContext.getDn(), ByPassConstants.LOOKUP_BYPASS );
331         List<Modification> items = opContext.getModItems();
332 
333         for ( Modification item : items )
334         {
335             if ( item.getOperation() == ModificationOperation.ADD_ATTRIBUTE )
336             {
337                 EntryAttribute modAttr = (ServerAttribute)item.getAttribute();
338                 EntryAttribute entryAttr = entry.get( modAttr.getId() );
339 
340                 if ( entryAttr != null )
341                 {
342                     for ( Value<?> value:modAttr )
343                     {
344                         if ( entryAttr.contains( value ) )
345                         {
346                             throw new LdapAttributeInUseException( "Trying to add existing value '" + value
347                                     + "' to attribute " + modAttr.getId() );
348                         }
349                     }
350                 }
351             }
352         }
353 
354         // Let's assume that the new modified entry may be an alias,
355         // but we don't want to check that now...
356         // We will simply remove the DN from the NotAlias cache.
357         // It would be smarter to check the modified attributes, but
358         // it would also be more complex.
359         synchronized( notAliasCache )
360         {
361             if ( notAliasCache.containsKey( opContext.getDn().getNormName() ) )
362             {
363                 notAliasCache.remove( opContext.getDn().getNormName() );
364             }
365         }
366 
367         nextInterceptor.modify( opContext );
368     }
369 
370     /**
371      * Checks to see the entry being renamed exists, otherwise throws the appropriate LdapException.
372      */
373     public void rename( NextInterceptor nextInterceptor, RenameOperationContext opContext )
374         throws Exception
375     {
376         LdapDN dn = opContext.getDn();
377         
378         if ( dn.getNormName().equalsIgnoreCase( subschemSubentryDn.getNormName() ) )
379         {
380             throw new LdapOperationNotSupportedException( 
381                 "Can not allow the renaming of the subschemaSubentry (" + 
382                 subschemSubentryDn + ") for the global schema: it is fixed at " + subschemSubentryDn,
383                 ResultCodeEnum.UNWILLING_TO_PERFORM );
384         }
385         
386         // check if entry to rename exists
387         String msg = "Attempt to rename non-existant entry: ";
388         assertHasEntry( nextInterceptor, opContext, msg, dn );
389 
390         // check to see if target entry exists
391         LdapDN newDn = ( LdapDN ) dn.clone();
392         newDn.remove( dn.size() - 1 );
393         newDn.add( opContext.getNewRdn() );
394         newDn.normalize( normalizerMap );
395         
396         if ( nextInterceptor.hasEntry( new EntryOperationContext( opContext.getSession(), newDn ) ) )
397         {
398             LdapNameAlreadyBoundException e;
399             e = new LdapNameAlreadyBoundException( "target entry " + newDn.getUpName() + " already exists!" );
400             e.setResolvedName( new LdapDN( newDn.getUpName() ) );
401             throw e;
402         }
403 
404         // Remove the previous entry from the notAnAlias cache
405         synchronized( notAliasCache )
406         {
407             if ( notAliasCache.containsKey( dn.getNormName() ) )
408             {
409                 notAliasCache.remove( dn.getNormName() );
410             }
411         }
412 
413         nextInterceptor.rename( opContext );
414     }
415 
416 
417     /**
418      * Checks to see the entry being moved exists, and so does its parent, otherwise throws the appropriate
419      * LdapException.
420      */
421     public void move( NextInterceptor nextInterceptor, MoveOperationContext opContext ) throws Exception
422     {
423         LdapDN oriChildName = opContext.getDn();
424         LdapDN newParentName = opContext.getParent();
425         
426         if ( oriChildName.getNormName().equalsIgnoreCase( subschemSubentryDn.getNormName() ) )
427         {
428             throw new LdapOperationNotSupportedException( 
429                 "Can not allow the move of the subschemaSubentry (" + 
430                 subschemSubentryDn + ") for the global schema: it is fixed at " + subschemSubentryDn,
431                 ResultCodeEnum.UNWILLING_TO_PERFORM );
432         }
433         
434         // check if child to move exists
435         String msg = "Attempt to move to non-existant parent: ";
436         assertHasEntry( nextInterceptor, opContext, msg, oriChildName );
437 
438         // check if parent to move to exists
439         msg = "Attempt to move to non-existant parent: ";
440         assertHasEntry( nextInterceptor, opContext, msg, newParentName );
441 
442         // check to see if target entry exists
443         String rdn = oriChildName.get( oriChildName.size() - 1 );
444         LdapDN target = ( LdapDN ) newParentName.clone();
445         target.add( rdn );
446         
447         if ( nextInterceptor.hasEntry( new EntryOperationContext( opContext.getSession(), target ) ) )
448         {
449             // we must calculate the resolved name using the user provided Rdn value
450             String upRdn = new LdapDN( oriChildName.getUpName() ).get( oriChildName.size() - 1 );
451             LdapDN upTarget = ( LdapDN ) newParentName.clone();
452             upTarget.add( upRdn );
453 
454             LdapNameAlreadyBoundException e;
455             e = new LdapNameAlreadyBoundException( "target entry " + upTarget.getUpName() + " already exists!" );
456             e.setResolvedName( new LdapDN( upTarget.getUpName() ) );
457             throw e;
458         }
459 
460         // Remove the original entry from the NotAlias cache, if needed
461         synchronized( notAliasCache )
462         {
463             if ( notAliasCache.containsKey( oriChildName.getNormName() ) )
464             {
465                 notAliasCache.remove( oriChildName.getNormName() );
466             }
467         }
468                 
469         nextInterceptor.move( opContext );
470     }
471 
472 
473     /**
474      * Checks to see the entry being moved exists, and so does its parent, otherwise throws the appropriate
475      * LdapException.
476      */
477     public void moveAndRename( NextInterceptor nextInterceptor, MoveAndRenameOperationContext opContext ) throws Exception
478     {
479         LdapDN oriChildName = opContext.getDn();
480         LdapDN parent = opContext.getParent();
481 
482         if ( oriChildName.getNormName().equalsIgnoreCase( subschemSubentryDn.getNormName() ) )
483         {
484             throw new LdapOperationNotSupportedException( 
485                 "Can not allow the move of the subschemaSubentry (" + 
486                 subschemSubentryDn + ") for the global schema: it is fixed at " + subschemSubentryDn,
487                 ResultCodeEnum.UNWILLING_TO_PERFORM );
488         }
489         
490         // check if child to move exists
491         String msg = "Attempt to move to non-existant parent: ";
492         assertHasEntry( nextInterceptor, opContext, msg, oriChildName );
493 
494         // check if parent to move to exists
495         msg = "Attempt to move to non-existant parent: ";
496         assertHasEntry( nextInterceptor, opContext, msg, parent );
497 
498         // check to see if target entry exists
499         LdapDN target = ( LdapDN ) parent.clone();
500         target.add( opContext.getNewRdn() );
501 
502         if ( nextInterceptor.hasEntry( new EntryOperationContext( opContext.getSession(), target ) ) )
503         {
504             // we must calculate the resolved name using the user provided Rdn value
505             LdapDN upTarget = ( LdapDN ) parent.clone();
506             upTarget.add( opContext.getNewRdn() );
507 
508             LdapNameAlreadyBoundException e;
509             e = new LdapNameAlreadyBoundException( "target entry " + upTarget.getUpName() + " already exists!" );
510             e.setResolvedName( new LdapDN( upTarget.getUpName() ) );
511             throw e;
512         }
513 
514         // Remove the original entry from the NotAlias cache, if needed
515         synchronized( notAliasCache )
516         {
517             if ( notAliasCache.containsKey( oriChildName.getNormName() ) )
518             {
519                 notAliasCache.remove( oriChildName.getNormName() );
520             }
521         }
522         
523         nextInterceptor.moveAndRename( opContext );
524     }
525 
526 
527     /**
528      * Checks to see the entry being searched exists, otherwise throws the appropriate LdapException.
529      */
530     public EntryFilteringCursor search( NextInterceptor nextInterceptor, SearchOperationContext opContext ) throws Exception
531     {
532         LdapDN base = opContext.getDn();
533 
534         try
535         {
536             EntryFilteringCursor cursor =  nextInterceptor.search( opContext );
537 	        
538             if ( ! cursor.next() )
539             {
540                 if ( !base.isEmpty() && !( subschemSubentryDn.toNormName() ).equalsIgnoreCase( base.toNormName() ) )
541                 {
542                     // We just check that the entry exists only if we didn't found any entry
543                     assertHasEntry( nextInterceptor, opContext, "Attempt to search under non-existant entry:" , base );
544                 }
545             }
546 
547             return cursor;
548         }
549         catch ( Exception ne )
550         {
551             String msg = "Attempt to search under non-existant entry: ";
552             assertHasEntry( nextInterceptor, opContext, msg, base );
553             throw ne;
554         }
555     }
556 
557 
558     /**
559      * Asserts that an entry is present and as a side effect if it is not, creates a LdapNameNotFoundException, which is
560      * used to set the before exception on the invocation - eventually the exception is thrown.
561      *
562      * @param msg        the message to prefix to the distinguished name for explanation
563      * @param dn         the distinguished name of the entry that is asserted
564      * @throws Exception if the entry does not exist
565      * @param nextInterceptor the next interceptor in the chain
566      */
567     private void assertHasEntry( NextInterceptor nextInterceptor, OperationContext opContext, 
568         String msg, LdapDN dn ) throws Exception
569     {
570         if ( subschemSubentryDn.getNormName().equals( dn.getNormName() ) )
571         {
572             return;
573         }
574         
575         if ( ! opContext.hasEntry( dn, ByPassConstants.HAS_ENTRY_BYPASS ) )
576         {
577             LdapNameNotFoundException e;
578 
579             if ( msg != null )
580             {
581                 e = new LdapNameNotFoundException( msg + dn.getUpName() );
582             }
583             else
584             {
585                 e = new LdapNameNotFoundException( dn.getUpName() );
586             }
587 
588             e.setResolvedName( 
589                 new LdapDN( 
590                     opContext.getSession().getDirectoryService().getOperationManager().getMatchedName( 
591                         new GetMatchedNameOperationContext( opContext.getSession(), dn ) ).getUpName() ) );
592             throw e;
593         }
594     }
595 }