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.exception;
021
022
023 import java.util.List;
024
025 import org.apache.commons.collections.map.LRUMap;
026 import org.apache.directory.server.core.DirectoryService;
027 import org.apache.directory.server.core.entry.ClonedServerEntry;
028 import org.apache.directory.server.core.filtering.BaseEntryFilteringCursor;
029 import org.apache.directory.server.core.filtering.EntryFilteringCursor;
030 import org.apache.directory.server.core.interceptor.BaseInterceptor;
031 import org.apache.directory.server.core.interceptor.NextInterceptor;
032 import org.apache.directory.server.core.interceptor.context.AddOperationContext;
033 import org.apache.directory.server.core.interceptor.context.DeleteOperationContext;
034 import org.apache.directory.server.core.interceptor.context.EntryOperationContext;
035 import org.apache.directory.server.core.interceptor.context.GetSuffixOperationContext;
036 import org.apache.directory.server.core.interceptor.context.ListOperationContext;
037 import org.apache.directory.server.core.interceptor.context.LookupOperationContext;
038 import org.apache.directory.server.core.interceptor.context.ModifyOperationContext;
039 import org.apache.directory.server.core.interceptor.context.MoveAndRenameOperationContext;
040 import org.apache.directory.server.core.interceptor.context.MoveOperationContext;
041 import org.apache.directory.server.core.interceptor.context.OperationContext;
042 import org.apache.directory.server.core.interceptor.context.RenameOperationContext;
043 import org.apache.directory.server.core.interceptor.context.SearchOperationContext;
044 import org.apache.directory.server.core.partition.ByPassConstants;
045 import org.apache.directory.server.core.partition.Partition;
046 import org.apache.directory.server.core.partition.PartitionNexus;
047 import org.apache.directory.server.i18n.I18n;
048 import org.apache.directory.shared.ldap.constants.SchemaConstants;
049 import org.apache.directory.shared.ldap.cursor.EmptyCursor;
050 import org.apache.directory.shared.ldap.entry.EntryAttribute;
051 import org.apache.directory.shared.ldap.entry.Modification;
052 import org.apache.directory.shared.ldap.entry.ModificationOperation;
053 import org.apache.directory.shared.ldap.entry.ServerEntry;
054 import org.apache.directory.shared.ldap.entry.Value;
055 import org.apache.directory.shared.ldap.exception.LdapAliasException;
056 import org.apache.directory.shared.ldap.exception.LdapAttributeInUseException;
057 import org.apache.directory.shared.ldap.exception.LdapContextNotEmptyException;
058 import org.apache.directory.shared.ldap.exception.LdapEntryAlreadyExistsException;
059 import org.apache.directory.shared.ldap.exception.LdapNoSuchObjectException;
060 import org.apache.directory.shared.ldap.exception.LdapUnwillingToPerformException;
061 import org.apache.directory.shared.ldap.message.ResultCodeEnum;
062 import org.apache.directory.shared.ldap.name.DN;
063
064
065 /**
066 * An {@link org.apache.directory.server.core.interceptor.Interceptor} that detects any operations that breaks integrity
067 * of {@link Partition} and terminates the current invocation chain by
068 * throwing a {@link Exception}. Those operations include when an entry
069 * already exists at a DN and is added once again to the same DN.
070 *
071 * @org.apache.xbean.XBean
072 *
073 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
074 * @version $Rev: 927839 $
075 */
076 public class ExceptionInterceptor extends BaseInterceptor
077 {
078 private PartitionNexus nexus;
079 private DirectoryService directoryService;
080 private DN subschemSubentryDn;
081
082
083 /**
084 * A cache to store entries which are not aliases.
085 * It's a speedup, we will be able to avoid backend lookups.
086 *
087 * Note that the backend also use a cache mechanism, but for performance gain, it's good
088 * to manage a cache here. The main problem is that when a user modify the parent, we will
089 * have to update it at three different places :
090 * - in the backend,
091 * - in the partition cache,
092 * - in this cache.
093 *
094 * The update of the backend and partition cache is already correctly handled, so we will
095 * just have to offer an access to refresh the local cache. This should be done in
096 * delete, modify and move operations.
097 *
098 * We need to be sure that frequently used DNs are always in cache, and not discarded.
099 * We will use a LRU cache for this purpose.
100 */
101 private final LRUMap notAliasCache = new LRUMap( DEFAULT_CACHE_SIZE );
102
103 /** Declare a default for this cache. 100 entries seems to be enough */
104 private static final int DEFAULT_CACHE_SIZE = 100;
105
106
107 /**
108 * Creates an interceptor that is also the exception handling service.
109 */
110 public ExceptionInterceptor()
111 {
112 }
113
114
115 public void init( DirectoryService directoryService ) throws Exception
116 {
117 this.directoryService = directoryService;
118 nexus = directoryService.getPartitionNexus();
119 Value<?> attr = nexus.getRootDSE( null ).get( SchemaConstants.SUBSCHEMA_SUBENTRY_AT ).get();
120 subschemSubentryDn = new DN( attr.getString() );
121 subschemSubentryDn.normalize( directoryService.getSchemaManager().getNormalizerMapping() );
122 }
123
124
125 public void destroy()
126 {
127 }
128
129 /**
130 * In the pre-invocation state this interceptor method checks to see if the entry to be added already exists. If it
131 * does an exception is raised.
132 */
133 public void add( NextInterceptor nextInterceptor, AddOperationContext opContext )
134 throws Exception
135 {
136 DN name = opContext.getDn();
137
138 if ( subschemSubentryDn.getNormName().equals( name.getNormName() ) )
139 {
140 throw new LdapEntryAlreadyExistsException( I18n.err( I18n.ERR_249 ) );
141 }
142
143 // check if the entry already exists
144 if ( nextInterceptor.hasEntry( new EntryOperationContext( opContext.getSession(), name ) ) )
145 {
146 LdapEntryAlreadyExistsException ne = new LdapEntryAlreadyExistsException( I18n.err( I18n.ERR_250, name.getName() ) );
147 //ne.setResolvedName( new DN( name.getName() ) );
148 throw ne;
149 }
150
151 DN suffix = nexus.getSuffix( new GetSuffixOperationContext( this.directoryService.getAdminSession(),
152 name ) );
153
154 // we're adding the suffix entry so just ignore stuff to mess with the parent
155 if ( suffix.equals( name ) )
156 {
157 nextInterceptor.add( opContext );
158 return;
159 }
160
161 DN parentDn = ( DN ) name.clone();
162 parentDn.remove( name.size() - 1 );
163
164 // check if we're trying to add to a parent that is an alias
165 boolean notAnAlias;
166
167 synchronized( notAliasCache )
168 {
169 notAnAlias = notAliasCache.containsKey( parentDn.getNormName() );
170 }
171
172 if ( ! notAnAlias )
173 {
174 // We don't know if the parent is an alias or not, so we will launch a
175 // lookup, and update the cache if it's not an alias
176 ClonedServerEntry attrs;
177
178 try
179 {
180 attrs = opContext.lookup( parentDn, ByPassConstants.LOOKUP_BYPASS );
181 }
182 catch ( Exception e )
183 {
184 LdapNoSuchObjectException e2 = new LdapNoSuchObjectException( I18n.err( I18n.ERR_251,
185 parentDn.getName() ) );
186 //e2.setResolvedName( new DN( nexus.getMatchedName(
187 // new GetMatchedNameOperationContext( opContext.getSession(), parentDn ) ).getName() ) );
188 throw e2;
189 }
190
191 EntryAttribute objectClass = attrs.getOriginalEntry().get( SchemaConstants.OBJECT_CLASS_AT );
192
193 if ( objectClass.contains( SchemaConstants.ALIAS_OC ) )
194 {
195 String msg = I18n.err( I18n.ERR_252, name.getName() );
196 LdapAliasException e = new LdapAliasException( msg );
197 //e.setResolvedName( new DN( parentDn.getName() ) );
198 throw e;
199 }
200 else
201 {
202 synchronized ( notAliasCache )
203 {
204 notAliasCache.put( parentDn.getNormName(), parentDn );
205 }
206 }
207 }
208
209 nextInterceptor.add( opContext );
210 }
211
212
213 /**
214 * Checks to make sure the entry being deleted exists, and has no children, otherwise throws the appropriate
215 * LdapException.
216 */
217 public void delete( NextInterceptor nextInterceptor, DeleteOperationContext opContext ) throws Exception
218 {
219 DN name = opContext.getDn();
220
221 if ( name.getNormName().equalsIgnoreCase( subschemSubentryDn.getNormName() ) )
222 {
223 throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM,
224 I18n.err( I18n.ERR_253, subschemSubentryDn ) );
225 }
226
227 // check if entry to delete exists
228 String msg = "Attempt to delete non-existant entry: ";
229 assertHasEntry( nextInterceptor, opContext, msg, name );
230
231 // check if entry to delete has children (only leaves can be deleted)
232 boolean hasChildren = false;
233 EntryFilteringCursor list = nextInterceptor.list( new ListOperationContext( opContext.getSession(), name ) );
234
235 if ( list.next() )
236 {
237 hasChildren = true;
238 }
239
240 list.close();
241
242 if ( hasChildren )
243 {
244 LdapContextNotEmptyException e = new LdapContextNotEmptyException();
245 //e.setResolvedName( new DN( name.getName() ) );
246 throw e;
247 }
248
249 synchronized( notAliasCache )
250 {
251 if ( notAliasCache.containsKey( name.getNormName() ) )
252 {
253 notAliasCache.remove( name.getNormName() );
254 }
255 }
256
257 nextInterceptor.delete( opContext );
258 }
259
260
261 /**
262 * Checks to see the base being searched exists, otherwise throws the appropriate LdapException.
263 */
264 public EntryFilteringCursor list( NextInterceptor nextInterceptor, ListOperationContext opContext ) throws Exception
265 {
266 if ( opContext.getDn().getNormName().equals( subschemSubentryDn.getNormName() ) )
267 {
268 // there is nothing under the schema subentry
269 return new BaseEntryFilteringCursor( new EmptyCursor<ServerEntry>(), opContext );
270 }
271
272 // check if entry to search exists
273 String msg = "Attempt to search under non-existant entry: ";
274 assertHasEntry( nextInterceptor, opContext, msg, opContext.getDn() );
275
276 return nextInterceptor.list( opContext );
277 }
278
279
280 /**
281 * Checks to see the base being searched exists, otherwise throws the appropriate LdapException.
282 */
283 public ClonedServerEntry lookup( NextInterceptor nextInterceptor, LookupOperationContext opContext ) throws Exception
284 {
285 if ( opContext.getDn().getNormName().equals( subschemSubentryDn.getNormName() ) )
286 {
287 return nexus.getRootDSE( null );
288 }
289
290 // check if entry to lookup exists
291 String msg = "Attempt to lookup non-existant entry: ";
292 assertHasEntry( nextInterceptor, opContext, msg, opContext.getDn() );
293
294 return nextInterceptor.lookup( opContext );
295 }
296
297
298 /**
299 * Checks to see the entry being modified exists, otherwise throws the appropriate LdapException.
300 */
301 public void modify( NextInterceptor nextInterceptor, ModifyOperationContext opContext )
302 throws Exception
303 {
304 // check if entry to modify exists
305 String msg = "Attempt to modify non-existant entry: ";
306
307 // handle operations against the schema subentry in the schema service
308 // and never try to look it up in the nexus below
309 if ( opContext.getDn().getNormName().equalsIgnoreCase( subschemSubentryDn.getNormName() ) )
310 {
311 nextInterceptor.modify( opContext );
312 return;
313 }
314
315 assertHasEntry( nextInterceptor, opContext, msg, opContext.getDn() );
316
317 ServerEntry entry = opContext.lookup( opContext.getDn(), ByPassConstants.LOOKUP_BYPASS );
318 List<Modification> items = opContext.getModItems();
319
320 for ( Modification item : items )
321 {
322 if ( item.getOperation() == ModificationOperation.ADD_ATTRIBUTE )
323 {
324 EntryAttribute modAttr = item.getAttribute();
325 EntryAttribute entryAttr = entry.get( modAttr.getId() );
326
327 if ( entryAttr != null )
328 {
329 for ( Value<?> value:modAttr )
330 {
331 if ( entryAttr.contains( value ) )
332 {
333 throw new LdapAttributeInUseException( I18n.err( I18n.ERR_254, value,
334 modAttr.getId() ) );
335 }
336 }
337 }
338 }
339 }
340
341 // Let's assume that the new modified entry may be an alias,
342 // but we don't want to check that now...
343 // We will simply remove the DN from the NotAlias cache.
344 // It would be smarter to check the modified attributes, but
345 // it would also be more complex.
346 synchronized( notAliasCache )
347 {
348 if ( notAliasCache.containsKey( opContext.getDn().getNormName() ) )
349 {
350 notAliasCache.remove( opContext.getDn().getNormName() );
351 }
352 }
353
354 nextInterceptor.modify( opContext );
355 }
356
357 /**
358 * Checks to see the entry being renamed exists, otherwise throws the appropriate LdapException.
359 */
360 public void rename( NextInterceptor nextInterceptor, RenameOperationContext opContext )
361 throws Exception
362 {
363 DN dn = opContext.getDn();
364
365 if ( dn.equals( subschemSubentryDn ) )
366 {
367 throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM, I18n.err( I18n.ERR_255, subschemSubentryDn,
368 subschemSubentryDn ) );
369 }
370
371 // Check to see if the renamed entry exists
372 if ( opContext.getEntry() == null )
373 {
374 // This is a nonsense : we can't rename an entry which does not exist
375 // on the server
376 LdapNoSuchObjectException ldnfe;
377 ldnfe = new LdapNoSuchObjectException( I18n.err( I18n.ERR_256, dn.getName() ) );
378 //ldnfe.setResolvedName( new DN( dn.getName() ) );
379 throw ldnfe;
380 }
381
382 // check to see if target entry exists
383 DN newDn = opContext.getNewDn();
384
385 if ( nextInterceptor.hasEntry( new EntryOperationContext( opContext.getSession(), newDn ) ) )
386 {
387 LdapEntryAlreadyExistsException e;
388 e = new LdapEntryAlreadyExistsException( I18n.err( I18n.ERR_257, newDn.getName() ) );
389 //e.setResolvedName( new DN( newDn.getName() ) );
390 throw e;
391 }
392
393 // Remove the previous entry from the notAnAlias cache
394 synchronized( notAliasCache )
395 {
396 if ( notAliasCache.containsKey( dn.getNormName() ) )
397 {
398 notAliasCache.remove( dn.getNormName() );
399 }
400 }
401
402 nextInterceptor.rename( opContext );
403 }
404
405
406 /**
407 * Checks to see the entry being moved exists, and so does its parent, otherwise throws the appropriate
408 * LdapException.
409 */
410 public void move( NextInterceptor nextInterceptor, MoveOperationContext opContext ) throws Exception
411 {
412 DN oriChildName = opContext.getDn();
413 DN newParentName = opContext.getParent();
414
415 if ( oriChildName.getNormName().equalsIgnoreCase( subschemSubentryDn.getNormName() ) )
416 {
417 throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM, I18n.err( I18n.ERR_258, subschemSubentryDn,
418 subschemSubentryDn ) );
419 }
420
421 // check if child to move exists
422 String msg = "Attempt to move to non-existant parent: ";
423 assertHasEntry( nextInterceptor, opContext, msg, oriChildName );
424
425 // check if parent to move to exists
426 msg = "Attempt to move to non-existant parent: ";
427 assertHasEntry( nextInterceptor, opContext, msg, newParentName );
428
429 // check to see if target entry exists
430 String rdn = oriChildName.get( oriChildName.size() - 1 );
431 DN target = ( DN ) newParentName.clone();
432 target.add( rdn );
433
434 if ( nextInterceptor.hasEntry( new EntryOperationContext( opContext.getSession(), target ) ) )
435 {
436 // we must calculate the resolved name using the user provided Rdn value
437 String upRdn = new DN( oriChildName.getName() ).get( oriChildName.size() - 1 );
438 DN upTarget = ( DN ) newParentName.clone();
439 upTarget.add( upRdn );
440
441 LdapEntryAlreadyExistsException e;
442 e = new LdapEntryAlreadyExistsException( I18n.err( I18n.ERR_257, upTarget.getName() ) );
443 //e.setResolvedName( new DN( upTarget.getName() ) );
444 throw e;
445 }
446
447 // Remove the original entry from the NotAlias cache, if needed
448 synchronized( notAliasCache )
449 {
450 if ( notAliasCache.containsKey( oriChildName.getNormName() ) )
451 {
452 notAliasCache.remove( oriChildName.getNormName() );
453 }
454 }
455
456 nextInterceptor.move( opContext );
457 }
458
459
460 /**
461 * Checks to see the entry being moved exists, and so does its parent, otherwise throws the appropriate
462 * LdapException.
463 */
464 public void moveAndRename( NextInterceptor nextInterceptor, MoveAndRenameOperationContext opContext ) throws Exception
465 {
466 DN oriChildName = opContext.getDn();
467 DN parent = opContext.getParent();
468
469 if ( oriChildName.getNormName().equalsIgnoreCase( subschemSubentryDn.getNormName() ) )
470 {
471 throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM, I18n.err( I18n.ERR_258, subschemSubentryDn,
472 subschemSubentryDn ) );
473 }
474
475 // check if child to move exists
476 String msg = "Attempt to move to non-existant parent: ";
477 assertHasEntry( nextInterceptor, opContext, msg, oriChildName );
478
479 // check if parent to move to exists
480 msg = "Attempt to move to non-existant parent: ";
481 assertHasEntry( nextInterceptor, opContext, msg, parent );
482
483 // check to see if target entry exists
484 DN target = ( DN ) parent.clone();
485 target.add( opContext.getNewRdn() );
486
487 if ( nextInterceptor.hasEntry( new EntryOperationContext( opContext.getSession(), target ) ) )
488 {
489 // we must calculate the resolved name using the user provided Rdn value
490 DN upTarget = ( DN ) parent.clone();
491 upTarget.add( opContext.getNewRdn() );
492
493 LdapEntryAlreadyExistsException e;
494 e = new LdapEntryAlreadyExistsException( I18n.err( I18n.ERR_257, upTarget.getName() ) );
495 //e.setResolvedName( new DN( upTarget.getName() ) );
496 throw e;
497 }
498
499 // Remove the original entry from the NotAlias cache, if needed
500 synchronized( notAliasCache )
501 {
502 if ( notAliasCache.containsKey( oriChildName.getNormName() ) )
503 {
504 notAliasCache.remove( oriChildName.getNormName() );
505 }
506 }
507
508 nextInterceptor.moveAndRename( opContext );
509 }
510
511
512 /**
513 * Checks to see the entry being searched exists, otherwise throws the appropriate LdapException.
514 */
515 public EntryFilteringCursor search( NextInterceptor nextInterceptor, SearchOperationContext opContext ) throws Exception
516 {
517 DN base = opContext.getDn();
518
519 try
520 {
521 EntryFilteringCursor cursor = nextInterceptor.search( opContext );
522
523 if ( ! cursor.next() )
524 {
525 if ( !base.isEmpty() && !( subschemSubentryDn.getNormName() ).equalsIgnoreCase( base.getNormName() ) )
526 {
527 // We just check that the entry exists only if we didn't found any entry
528 assertHasEntry( nextInterceptor, opContext, "Attempt to search under non-existant entry:" , base );
529 }
530 }
531
532 return cursor;
533 }
534 catch ( Exception ne )
535 {
536 String msg = I18n.err( I18n.ERR_259 );
537 assertHasEntry( nextInterceptor, opContext, msg, base );
538 throw ne;
539 }
540 }
541
542
543 /**
544 * Asserts that an entry is present and as a side effect if it is not, creates a LdapNoSuchObjectException, which is
545 * used to set the before exception on the invocation - eventually the exception is thrown.
546 *
547 * @param msg the message to prefix to the distinguished name for explanation
548 * @param dn the distinguished name of the entry that is asserted
549 * @throws Exception if the entry does not exist
550 * @param nextInterceptor the next interceptor in the chain
551 */
552 private void assertHasEntry( NextInterceptor nextInterceptor, OperationContext opContext,
553 String msg, DN dn ) throws Exception
554 {
555 if ( subschemSubentryDn.getNormName().equals( dn.getNormName() ) )
556 {
557 return;
558 }
559
560 if ( ! opContext.hasEntry( dn, ByPassConstants.HAS_ENTRY_BYPASS ) )
561 {
562 LdapNoSuchObjectException e;
563
564 if ( msg != null )
565 {
566 e = new LdapNoSuchObjectException( msg + dn.getName() );
567 }
568 else
569 {
570 e = new LdapNoSuchObjectException( dn.getName() );
571 }
572
573 //e.setResolvedName(
574 // new DN(
575 // opContext.getSession().getDirectoryService().getOperationManager().getMatchedName(
576 // new GetMatchedNameOperationContext( opContext.getSession(), dn ) ).getName() ) );
577 throw e;
578 }
579 }
580 }