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 */ 020package org.apache.directory.server.core.exception; 021 022 023import org.apache.commons.collections4.map.LRUMap; 024import org.apache.directory.api.ldap.model.constants.SchemaConstants; 025import org.apache.directory.api.ldap.model.entry.Attribute; 026import org.apache.directory.api.ldap.model.entry.Entry; 027import org.apache.directory.api.ldap.model.entry.Value; 028import org.apache.directory.api.ldap.model.exception.LdapAliasException; 029import org.apache.directory.api.ldap.model.exception.LdapEntryAlreadyExistsException; 030import org.apache.directory.api.ldap.model.exception.LdapException; 031import org.apache.directory.api.ldap.model.exception.LdapNoSuchObjectException; 032import org.apache.directory.api.ldap.model.exception.LdapUnwillingToPerformException; 033import org.apache.directory.api.ldap.model.message.ResultCodeEnum; 034import org.apache.directory.api.ldap.model.name.Dn; 035import org.apache.directory.server.core.api.CoreSession; 036import org.apache.directory.server.core.api.DirectoryService; 037import org.apache.directory.server.core.api.InterceptorEnum; 038import org.apache.directory.server.core.api.entry.ClonedServerEntry; 039import org.apache.directory.server.core.api.interceptor.BaseInterceptor; 040import org.apache.directory.server.core.api.interceptor.Interceptor; 041import org.apache.directory.server.core.api.interceptor.context.AddOperationContext; 042import org.apache.directory.server.core.api.interceptor.context.DeleteOperationContext; 043import org.apache.directory.server.core.api.interceptor.context.HasEntryOperationContext; 044import org.apache.directory.server.core.api.interceptor.context.LookupOperationContext; 045import org.apache.directory.server.core.api.interceptor.context.ModifyOperationContext; 046import org.apache.directory.server.core.api.interceptor.context.MoveAndRenameOperationContext; 047import org.apache.directory.server.core.api.interceptor.context.MoveOperationContext; 048import org.apache.directory.server.core.api.interceptor.context.OperationContext; 049import org.apache.directory.server.core.api.interceptor.context.RenameOperationContext; 050import org.apache.directory.server.core.api.partition.Partition; 051import org.apache.directory.server.core.api.partition.PartitionNexus; 052import org.apache.directory.server.i18n.I18n; 053 054 055/** 056 * An {@link Interceptor} that detects any operations that breaks integrity 057 * of {@link Partition} and terminates the current invocation chain by 058 * throwing a {@link Exception}. Those operations include when an entry 059 * already exists at a Dn and is added once again to the same Dn. 060 * 061 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a> 062 */ 063public class ExceptionInterceptor extends BaseInterceptor 064{ 065 private PartitionNexus nexus; 066 private Dn subschemSubentryDn; 067 068 /** 069 * A cache to store entries which are not aliases. 070 * It's a speedup, we will be able to avoid backend lookups. 071 * 072 * Note that the backend also use a cache mechanism, but for performance gain, it's good 073 * to manage a cache here. The main problem is that when a user modify the parent, we will 074 * have to update it at three different places : 075 * - in the backend, 076 * - in the partition cache, 077 * - in this cache. 078 * 079 * The update of the backend and partition cache is already correctly handled, so we will 080 * just have to offer an access to refresh the local cache. This should be done in 081 * delete, modify and move operations. 082 * 083 * We need to be sure that frequently used DNs are always in cache, and not discarded. 084 * We will use a LRU cache for this purpose. 085 */ 086 private final LRUMap notAliasCache = new LRUMap( DEFAULT_CACHE_SIZE ); 087 088 /** Declare a default for this cache. 100 entries seems to be enough */ 089 private static final int DEFAULT_CACHE_SIZE = 100; 090 091 092 /** 093 * Creates an interceptor that is also the exception handling service. 094 */ 095 public ExceptionInterceptor() 096 { 097 super( InterceptorEnum.EXCEPTION_INTERCEPTOR ); 098 } 099 100 101 /** 102 * {@inheritDoc} 103 */ 104 @Override 105 public void init( DirectoryService directoryService ) throws LdapException 106 { 107 super.init( directoryService ); 108 nexus = directoryService.getPartitionNexus(); 109 Value attr = nexus.getRootDseValue( directoryService.getAtProvider().getSubschemaSubentry() ); 110 subschemSubentryDn = dnFactory.create( attr.getString() ); 111 } 112 113 114 /** 115 * In the pre-invocation state this interceptor method checks to see if the entry to be added already exists. If it 116 * does an exception is raised. 117 */ 118 @Override 119 public void add( AddOperationContext addContext ) throws LdapException 120 { 121 Dn name = addContext.getDn(); 122 123 if ( subschemSubentryDn.equals( name ) ) 124 { 125 throw new LdapEntryAlreadyExistsException( I18n.err( I18n.ERR_249 ) ); 126 } 127 128 Dn suffix = nexus.getSuffixDn( name ); 129 130 // we're adding the suffix entry so just ignore stuff to mess with the parent 131 if ( suffix.equals( name ) ) 132 { 133 next( addContext ); 134 return; 135 } 136 137 Dn parentDn = name.getParent(); 138 139 // check if we're trying to add to a parent that is an alias 140 boolean notAnAlias; 141 142 synchronized ( notAliasCache ) 143 { 144 notAnAlias = notAliasCache.containsKey( parentDn.getNormName() ); 145 } 146 147 if ( !notAnAlias ) 148 { 149 // We don't know if the parent is an alias or not, so we will launch a 150 // lookup, and update the cache if it's not an alias 151 Entry attrs; 152 153 try 154 { 155 CoreSession session = addContext.getSession(); 156 LookupOperationContext lookupContext = new LookupOperationContext( session, parentDn, 157 SchemaConstants.ALL_ATTRIBUTES_ARRAY ); 158 lookupContext.setPartition( addContext.getPartition() ); 159 lookupContext.setTransaction( addContext.getTransaction() ); 160 161 attrs = directoryService.getPartitionNexus().lookup( lookupContext ); 162 } 163 catch ( Exception e ) 164 { 165 throw new LdapNoSuchObjectException( I18n.err( I18n.ERR_251_PARENT_NOT_FOUND, parentDn.getName() ) ); 166 } 167 168 Attribute objectClass = ( ( ClonedServerEntry ) attrs ).getOriginalEntry().get( 169 directoryService.getAtProvider().getObjectClass() ); 170 171 if ( objectClass.contains( SchemaConstants.ALIAS_OC ) ) 172 { 173 String msg = I18n.err( I18n.ERR_252_ALIAS_WITH_CHILD_NOT_ALLOWED, name.getName(), parentDn.getName() ); 174 throw new LdapAliasException( msg ); 175 } 176 else 177 { 178 synchronized ( notAliasCache ) 179 { 180 notAliasCache.put( parentDn.getNormName(), parentDn ); 181 } 182 } 183 } 184 185 next( addContext ); 186 } 187 188 189 /** 190 * Checks to make sure the entry being deleted exists, and has no children, otherwise throws the appropriate 191 * LdapException. 192 */ 193 @Override 194 public void delete( DeleteOperationContext deleteContext ) throws LdapException 195 { 196 Dn dn = deleteContext.getDn(); 197 198 if ( dn.equals( subschemSubentryDn ) ) 199 { 200 throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM, I18n.err( I18n.ERR_253, 201 subschemSubentryDn ) ); 202 } 203 204 next( deleteContext ); 205 206 // Update the alias cache 207 synchronized ( notAliasCache ) 208 { 209 if ( notAliasCache.containsKey( dn.getNormName() ) ) 210 { 211 notAliasCache.remove( dn.getNormName() ); 212 } 213 } 214 } 215 216 217 /** 218 * {@inheritDoc} 219 */ 220 @Override 221 public void modify( ModifyOperationContext modifyContext ) throws LdapException 222 { 223 // check if entry to modify exists 224 String msg = "Attempt to modify non-existant entry: "; 225 226 // handle operations against the schema subentry in the schema service 227 // and never try to look it up in the nexus below 228 if ( modifyContext.getDn().equals( subschemSubentryDn ) ) 229 { 230 next( modifyContext ); 231 return; 232 } 233 234 // Check that the entry we read at the beginning exists. If 235 // not, we will throw an exception here 236 assertHasEntry( modifyContext, msg ); 237 238 // Let's assume that the new modified entry may be an alias, 239 // but we don't want to check that now... 240 // We will simply remove the Dn from the NotAlias cache. 241 // It would be smarter to check the modified attributes, but 242 // it would also be more complex. 243 synchronized ( notAliasCache ) 244 { 245 if ( notAliasCache.containsKey( modifyContext.getDn().getNormName() ) ) 246 { 247 notAliasCache.remove( modifyContext.getDn().getNormName() ); 248 } 249 } 250 251 next( modifyContext ); 252 } 253 254 255 /** 256 * {@inheritDoc} 257 */ 258 @Override 259 public void move( MoveOperationContext moveContext ) throws LdapException 260 { 261 Dn oriChildName = moveContext.getDn(); 262 263 if ( oriChildName.equals( subschemSubentryDn ) ) 264 { 265 throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM, I18n.err( I18n.ERR_258, 266 subschemSubentryDn, subschemSubentryDn ) ); 267 } 268 269 next( moveContext ); 270 271 // Remove the original entry from the NotAlias cache, if needed 272 synchronized ( notAliasCache ) 273 { 274 if ( notAliasCache.containsKey( oriChildName.getNormName() ) ) 275 { 276 notAliasCache.remove( oriChildName.getNormName() ); 277 } 278 } 279 } 280 281 282 /** 283 * {@inheritDoc} 284 */ 285 @Override 286 public void moveAndRename( MoveAndRenameOperationContext moveAndRenameContext ) throws LdapException 287 { 288 Dn oldDn = moveAndRenameContext.getDn(); 289 290 // Don't allow M&R in the SSSE 291 if ( oldDn.getNormName().equals( subschemSubentryDn.getNormName() ) ) 292 { 293 throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM, I18n.err( I18n.ERR_258, 294 subschemSubentryDn, subschemSubentryDn ) ); 295 } 296 297 // Remove the original entry from the NotAlias cache, if needed 298 synchronized ( notAliasCache ) 299 { 300 if ( notAliasCache.containsKey( oldDn.getNormName() ) ) 301 { 302 notAliasCache.remove( oldDn.getNormName() ); 303 } 304 } 305 306 next( moveAndRenameContext ); 307 } 308 309 310 /** 311 * {@inheritDoc} 312 */ 313 @Override 314 public void rename( RenameOperationContext renameContext ) throws LdapException 315 { 316 Dn dn = renameContext.getDn(); 317 318 if ( dn.equals( subschemSubentryDn ) ) 319 { 320 throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM, I18n.err( I18n.ERR_255, 321 subschemSubentryDn, subschemSubentryDn ) ); 322 } 323 324 // check to see if target entry exists 325 Dn newDn = renameContext.getNewDn(); 326 327 HasEntryOperationContext hasEntryContext = new HasEntryOperationContext( renameContext.getSession(), newDn ); 328 hasEntryContext.setPartition( renameContext.getPartition() ); 329 hasEntryContext.setTransaction( renameContext.getTransaction() ); 330 331 if ( nexus.hasEntry( hasEntryContext ) ) 332 { 333 // Ok, the target entry already exists. 334 // If the target entry has the same name than the modified entry, it's a rename on itself, 335 // we want to allow this. 336 if ( !newDn.equals( dn ) ) 337 { 338 throw new LdapEntryAlreadyExistsException( I18n.err( I18n.ERR_250_ENTRY_ALREADY_EXISTS, newDn.getName() ) ); 339 } 340 } 341 342 // Remove the previous entry from the notAnAlias cache 343 synchronized ( notAliasCache ) 344 { 345 if ( notAliasCache.containsKey( dn.getNormName() ) ) 346 { 347 notAliasCache.remove( dn.getNormName() ); 348 } 349 } 350 351 next( renameContext ); 352 } 353 354 355 /** 356 * Asserts that an entry is present and as a side effect if it is not, creates a LdapNoSuchObjectException, which is 357 * used to set the before exception on the invocation - eventually the exception is thrown. 358 * 359 * @param msg the message to prefix to the distinguished name for explanation 360 * @throws Exception if the entry does not exist 361 * @param nextInterceptor the next interceptor in the chain 362 */ 363 private void assertHasEntry( OperationContext opContext, String msg ) throws LdapException 364 { 365 Dn dn = opContext.getDn(); 366 367 if ( subschemSubentryDn.equals( dn ) ) 368 { 369 return; 370 } 371 372 if ( opContext.getEntry() == null ) 373 { 374 LdapNoSuchObjectException e; 375 376 if ( msg != null ) 377 { 378 e = new LdapNoSuchObjectException( msg + dn.getName() ); 379 } 380 else 381 { 382 e = new LdapNoSuchObjectException( dn.getName() ); 383 } 384 385 throw e; 386 } 387 } 388 389 /** 390 * Asserts that an entry is present and as a side effect if it is not, creates a LdapNoSuchObjectException, which is 391 * used to set the before exception on the invocation - eventually the exception is thrown. 392 * 393 * @param msg the message to prefix to the distinguished name for explanation 394 * @param dn the distinguished name of the entry that is asserted 395 * @throws Exception if the entry does not exist 396 * @param nextInterceptor the next interceptor in the chain 397 * 398 private void assertHasEntry( OperationContext opContext, String msg, Dn dn ) throws LdapException 399 { 400 if ( subschemSubentryDn.equals( dn ) ) 401 { 402 return; 403 } 404 405 if ( !opContext.hasEntry( dn, ByPassConstants.HAS_ENTRY_BYPASS ) ) 406 { 407 LdapNoSuchObjectException e; 408 409 if ( msg != null ) 410 { 411 e = new LdapNoSuchObjectException( msg + dn.getName() ); 412 } 413 else 414 { 415 e = new LdapNoSuchObjectException( dn.getName() ); 416 } 417 418 throw e; 419 } 420 }*/ 421}