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.referral;
021
022
023import javax.naming.Context;
024
025import org.apache.directory.api.ldap.model.constants.SchemaConstants;
026import org.apache.directory.api.ldap.model.entry.Attribute;
027import org.apache.directory.api.ldap.model.entry.Entry;
028import org.apache.directory.api.ldap.model.entry.Value;
029import org.apache.directory.api.ldap.model.exception.LdapException;
030import org.apache.directory.api.ldap.model.exception.LdapURLEncodingException;
031import org.apache.directory.api.ldap.model.message.SearchScope;
032import org.apache.directory.api.ldap.model.name.Dn;
033import org.apache.directory.api.ldap.model.url.LdapUrl;
034import org.apache.directory.api.util.Strings;
035import org.apache.directory.server.core.api.DirectoryService;
036import org.apache.directory.server.core.api.InterceptorEnum;
037import org.apache.directory.server.core.api.ReferralManager;
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.context.AddOperationContext;
041import org.apache.directory.server.core.api.interceptor.context.DeleteOperationContext;
042import org.apache.directory.server.core.api.interceptor.context.LookupOperationContext;
043import org.apache.directory.server.core.api.interceptor.context.ModifyOperationContext;
044import org.apache.directory.server.core.api.interceptor.context.MoveAndRenameOperationContext;
045import org.apache.directory.server.core.api.interceptor.context.MoveOperationContext;
046import org.apache.directory.server.core.api.interceptor.context.RenameOperationContext;
047import org.apache.directory.server.core.api.partition.PartitionNexus;
048import org.apache.directory.server.core.shared.ReferralManagerImpl;
049import org.apache.directory.server.i18n.I18n;
050import org.slf4j.Logger;
051import org.slf4j.LoggerFactory;
052
053
054/**
055 * An service which is responsible referral handling behaviors.  It manages
056 * referral handling behavior when the {@link Context#REFERRAL} is implicitly
057 * or explicitly set to "ignore", when set to "throw" and when set to "follow".
058 * 
059 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
060 */
061public class ReferralInterceptor extends BaseInterceptor
062{
063    private static final Logger LOG = LoggerFactory.getLogger( ReferralInterceptor.class );
064
065    private PartitionNexus nexus;
066
067    /** The referralManager */
068    private ReferralManager referralManager;
069
070    /** A normalized form for the SubschemaSubentry Dn */
071    private Dn subschemaSubentryDn;
072
073
074    /**
075     * Creates a new instance of a ReferralInterceptor.
076     */
077    public ReferralInterceptor()
078    {
079        super( InterceptorEnum.REFERRAL_INTERCEPTOR );
080    }
081
082
083    private static void checkRefAttributeValue( Value value ) throws LdapException
084    {
085        String refVal = value.getString();
086
087        LdapUrl ldapUrl = new LdapUrl( refVal );
088
089        // We have a LDAP URL, we have to check that :
090        // - we don't have scope specifier
091        // - we don't have filters
092        // - we don't have attribute description list
093        // - we don't have extensions
094        // - the Dn is not empty
095
096        if ( ldapUrl.getScope() != SearchScope.OBJECT )
097        {
098            // This is the default value if we don't have any scope
099            // Let's assume that it's incorrect if we get something
100            // else in the LdapURL
101            String message = I18n.err( I18n.ERR_36 );
102            LOG.error( message );
103            throw new LdapException( message );
104        }
105
106        if ( !Strings.isEmpty( ldapUrl.getFilter() ) )
107        {
108            String message = I18n.err( I18n.ERR_37 );
109            LOG.error( message );
110            throw new LdapException( message );
111        }
112
113        if ( ( ldapUrl.getAttributes() != null ) && !ldapUrl.getAttributes().isEmpty() )
114        {
115            String message = I18n.err( I18n.ERR_38 );
116            LOG.error( message );
117            throw new LdapException( message );
118        }
119
120        if ( ( ldapUrl.getExtensions() != null ) && !ldapUrl.getExtensions().isEmpty() )
121        {
122            String message = I18n.err( I18n.ERR_39 );
123            LOG.error( message );
124            throw new LdapException( message );
125        }
126
127        if ( ( ldapUrl.getExtensions() != null ) && !ldapUrl.getExtensions().isEmpty() )
128        {
129            String message = I18n.err( I18n.ERR_40 );
130            LOG.error( message );
131            throw new LdapException( message );
132        }
133
134        Dn dn = ldapUrl.getDn();
135
136        if ( ( dn == null ) || dn.isEmpty() )
137        {
138            String message = I18n.err( I18n.ERR_41 );
139            LOG.error( message );
140            throw new LdapException( message );
141        }
142    }
143
144
145    // This will suppress PMD.EmptyCatchBlock warnings in this method
146    @SuppressWarnings("PMD.EmptyCatchBlock")
147    private boolean isReferral( Entry entry ) throws LdapException
148    {
149        // Check that the entry is not null, otherwise return FALSE.
150        // This is typically to cover the case where the entry has not
151        // been added into the context because it does not exists.
152        if ( entry == null )
153        {
154            return false;
155        }
156
157        if ( !entry.contains( directoryService.getAtProvider().getObjectClass(), SchemaConstants.REFERRAL_OC ) )
158        {
159            return false;
160        }
161        else
162        {
163            // We have a referral ObjectClass, let's check that the ref is
164            // valid, accordingly to the RFC
165
166            // Get the 'ref' attributeType
167            Attribute refAttr = entry.get( SchemaConstants.REF_AT );
168
169            if ( refAttr == null )
170            {
171                // very unlikely, as we have already checked the entry in SchemaInterceptor
172                String message = I18n.err( I18n.ERR_42 );
173                LOG.error( message );
174                throw new LdapException( message );
175            }
176
177            for ( Value value : refAttr )
178            {
179                try
180                {
181                    checkRefAttributeValue( value );
182                }
183                catch ( LdapURLEncodingException luee )
184                {
185                    // Either the URL is invalid, or it's not a LDAP URL.
186                    // we will just ignore this LdapURL.
187                }
188            }
189
190            return true;
191        }
192    }
193
194
195    @Override
196    public void init( DirectoryService directoryService ) throws LdapException
197    {
198        super.init( directoryService );
199
200        nexus = directoryService.getPartitionNexus();
201
202        // Initialize the referralManager
203        referralManager = new ReferralManagerImpl( directoryService );
204        directoryService.setReferralManager( referralManager );
205
206        Value subschemaSubentry = nexus.getRootDseValue( directoryService.getAtProvider().getSubschemaSubentry() );
207        subschemaSubentryDn = dnFactory.create( subschemaSubentry.getString() );
208    }
209
210
211    /**
212     * Add an entry into the server. We have 3 cases :
213     * (1) The entry does not have any parent referral and is not a referral itself
214     * (2) The entry does not have any parent referral and is a referral itself
215     * (3) The entry has a parent referral
216     * 
217     * Case (1) is easy : we inject the entry into the server and we are done.
218     * Case (2) is the same as case (1), but we have to update the referral manager.
219     * Case (3) is handled by the LdapProcotol handler, as we have to return a
220     * LdapResult containing a list of this entry's parent's referrals URL, if the
221     * ManageDSAIT control is not present, or the parent's entry if the control
222     * is present.
223     * 
224     * Of course, if the entry already exists, nothing will be done, as we will get an
225     * entryAlreadyExists error.
226     * 
227     */
228    /**
229     * {@inheritDoc}
230     */
231    @Override
232    public void add( AddOperationContext addContext ) throws LdapException
233    {
234        Entry entry = addContext.getEntry();
235
236        // Check if the entry is a referral itself
237        boolean isReferral = isReferral( entry );
238
239        // We add the entry into the server
240        next( addContext );
241
242        // If the addition is successful, we update the referralManager
243        if ( isReferral )
244        {
245            // We have to add it to the referralManager
246            referralManager.lockWrite();
247
248            try
249            {
250                referralManager.addReferral( entry );
251            }
252            finally
253            {
254                referralManager.unlock();
255            }
256        }
257    }
258
259
260    /**
261     * Delete an entry in the server. We have 4 cases :
262     * (1) the entry is not a referral and does not have a parent referral
263     * (2) the entry is not a referral but has a parent referral
264     * (3) the entry is a referral
265     * 
266     * Case (1) is handled by removing the entry from the server
267     * In case (2), we return an exception build using the parent referral
268     * For case(3), we remove the entry from the server and remove the referral
269     * from the referral manager.
270     * 
271     * If the entry does not exist in the server, we will get a NoSuchObject error
272     */
273    /**
274     * {@inheritDoc}
275     */
276    @Override
277    public void delete( DeleteOperationContext deleteContext ) throws LdapException
278    {
279        // First delete the entry into the server
280        next( deleteContext );
281
282        Entry entry = deleteContext.getEntry();
283
284        // Check if the entry exists and is a referral itself
285        // If so, we have to update the referralManager
286        if ( ( entry != null ) && isReferral( entry ) )
287        {
288            // We have to remove it from the referralManager
289            referralManager.lockWrite();
290
291            try
292            {
293                referralManager.removeReferral( entry );
294            }
295            finally
296            {
297                referralManager.unlock();
298            }
299        }
300    }
301
302
303    /**
304     * {@inheritDoc}
305     */
306    @Override
307    public void modify( ModifyOperationContext modifyContext ) throws LdapException
308    {
309        Dn dn = modifyContext.getDn();
310
311        // handle a normal modify without following referrals
312        next( modifyContext );
313
314        // Check if we are trying to modify the schema or the rootDSE,
315        // if so, we don't modify the referralManager
316        if ( dn.isEmpty() || dn.equals( subschemaSubentryDn ) )
317        {
318            // Do nothing
319            return;
320        }
321
322        // Update the referralManager. We have to read the entry again
323        // as it has been modified, before updating the ReferralManager
324        // TODO: this can be spare, as we already have the altered entry
325        // into the opContext, but for an unknow reason, this will fail
326        // on eferral tests...
327        LookupOperationContext lookupContext =
328            new LookupOperationContext( modifyContext.getSession(), dn, SchemaConstants.ALL_ATTRIBUTES_ARRAY );
329        lookupContext.setPartition( modifyContext.getPartition() );
330        lookupContext.setTransaction( modifyContext.getTransaction() );
331
332        Entry newEntry = nexus.lookup( lookupContext );
333
334        // Update the referralManager.
335        // Check that we have the entry, just in case
336        // TODO : entries should be locked until the operation is done on it.
337        if ( newEntry != null )
338        {
339            referralManager.lockWrite();
340
341            try
342            {
343                if ( referralManager.isReferral( newEntry.getDn() ) )
344                {
345                    referralManager.removeReferral( modifyContext.getEntry() );
346                    referralManager.addReferral( newEntry );
347                }
348            }
349            finally
350            {
351                referralManager.unlock();
352            }
353        }
354    }
355
356
357    /**
358     * {@inheritDoc}
359     **/
360    @Override
361    public void move( MoveOperationContext moveContext ) throws LdapException
362    {
363        // Check if the entry is a referral itself
364        boolean isReferral = isReferral( moveContext.getOriginalEntry() );
365
366        next( moveContext );
367
368        if ( isReferral )
369        {
370            // Update the referralManager
371            referralManager.lockWrite();
372
373            try
374            {
375                referralManager.addReferral( moveContext.getModifiedEntry() );
376                referralManager.removeReferral( moveContext.getOriginalEntry() );
377            }
378            finally
379            {
380                referralManager.unlock();
381            }
382        }
383    }
384
385
386    /**
387     * {@inheritDoc}
388     **/
389    @Override
390    public void moveAndRename( MoveAndRenameOperationContext moveAndRenameContext ) throws LdapException
391    {
392        // Check if the entry is a referral itself
393        boolean isReferral = isReferral( moveAndRenameContext.getOriginalEntry() );
394
395        next( moveAndRenameContext );
396
397        if ( isReferral )
398        {
399            // Update the referralManager
400            Entry newEntry = moveAndRenameContext.getModifiedEntry();
401
402            referralManager.lockWrite();
403
404            try
405            {
406                referralManager.addReferral( newEntry );
407                referralManager.removeReferral( moveAndRenameContext.getOriginalEntry() );
408            }
409            finally
410            {
411                referralManager.unlock();
412            }
413        }
414    }
415
416
417    /**
418     * {@inheritDoc}
419     **/
420    @Override
421    public void rename( RenameOperationContext renameContext ) throws LdapException
422    {
423        // Check if the entry is a referral itself
424        boolean isReferral = isReferral( renameContext.getOriginalEntry() );
425
426        next( renameContext );
427
428        if ( isReferral )
429        {
430            // Update the referralManager
431            LookupOperationContext lookupContext = new LookupOperationContext( renameContext.getSession(),
432                renameContext.getNewDn(), SchemaConstants.ALL_ATTRIBUTES_ARRAY );
433            lookupContext.setPartition( renameContext.getPartition() );
434            lookupContext.setTransaction( renameContext.getTransaction() );
435
436            Entry newEntry = nexus.lookup( lookupContext );
437
438            referralManager.lockWrite();
439
440            try
441            {
442                referralManager.addReferral( newEntry );
443                referralManager.removeReferral( ( ( ClonedServerEntry ) renameContext.getEntry() ).getOriginalEntry() );
444            }
445            finally
446            {
447                referralManager.unlock();
448            }
449        }
450    }
451}