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.collective;
021
022
023import java.util.HashSet;
024import java.util.List;
025import java.util.Set;
026
027import org.apache.directory.api.ldap.model.constants.SchemaConstants;
028import org.apache.directory.api.ldap.model.entry.Attribute;
029import org.apache.directory.api.ldap.model.entry.DefaultAttribute;
030import org.apache.directory.api.ldap.model.entry.Entry;
031import org.apache.directory.api.ldap.model.entry.Modification;
032import org.apache.directory.api.ldap.model.entry.ModificationOperation;
033import org.apache.directory.api.ldap.model.entry.Value;
034import org.apache.directory.api.ldap.model.exception.LdapException;
035import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeTypeException;
036import org.apache.directory.api.ldap.model.exception.LdapSchemaViolationException;
037import org.apache.directory.api.ldap.model.message.ResultCodeEnum;
038import org.apache.directory.api.ldap.model.name.Dn;
039import org.apache.directory.api.ldap.model.schema.AttributeType;
040import org.apache.directory.api.ldap.model.schema.SchemaUtils;
041import org.apache.directory.server.core.api.CoreSession;
042import org.apache.directory.server.core.api.DirectoryService;
043import org.apache.directory.server.core.api.InterceptorEnum;
044import org.apache.directory.server.core.api.entry.ClonedServerEntry;
045import org.apache.directory.server.core.api.filtering.EntryFilter;
046import org.apache.directory.server.core.api.filtering.EntryFilteringCursor;
047import org.apache.directory.server.core.api.interceptor.BaseInterceptor;
048import org.apache.directory.server.core.api.interceptor.context.AddOperationContext;
049import org.apache.directory.server.core.api.interceptor.context.FilteringOperationContext;
050import org.apache.directory.server.core.api.interceptor.context.LookupOperationContext;
051import org.apache.directory.server.core.api.interceptor.context.ModifyOperationContext;
052import org.apache.directory.server.core.api.interceptor.context.SearchOperationContext;
053import org.apache.directory.server.i18n.I18n;
054import org.slf4j.Logger;
055import org.slf4j.LoggerFactory;
056
057
058/**
059 * An interceptor based service dealing with collective attribute
060 * management.  This service intercepts read operations on entries to
061 * inject collective attribute value pairs into the response based on
062 * the entires inclusion within collectiveAttributeSpecificAreas and
063 * collectiveAttributeInnerAreas.
064 *
065 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
066 */
067public class CollectiveAttributeInterceptor extends BaseInterceptor
068{
069    /** The LoggerFactory used by this Interceptor */
070    private static final Logger LOG = LoggerFactory.getLogger( CollectiveAttributeInterceptor.class );
071
072
073    /**
074     * Creates a new instance of a CollectiveAttributeInterceptor.
075     */
076    public CollectiveAttributeInterceptor()
077    {
078        super( InterceptorEnum.COLLECTIVE_ATTRIBUTE_INTERCEPTOR );
079    }
080
081    /**
082     * the search result filter to use for collective attribute injection
083     */
084    private class CollectiveAttributeFilter implements EntryFilter
085    {
086        /**
087         * {@inheritDoc}
088         */
089        @Override
090        public boolean accept( SearchOperationContext operation, Entry entry ) throws LdapException
091        {
092            addCollectiveAttributes( operation, entry );
093
094            return true;
095        }
096
097
098        /**
099         * {@inheritDoc}
100         */
101        @Override
102        public String toString( String tabs )
103        {
104            return tabs + "CollectiveAttributeFilter";
105        }
106    }
107
108    /** The CollectiveAttribute search filter */
109    private final EntryFilter searchFilter = new CollectiveAttributeFilter();
110
111
112    //-------------------------------------------------------------------------------------
113    // Initialization
114    //-------------------------------------------------------------------------------------
115    /**
116     * {@inheritDoc}
117     */
118    @Override
119    public void init( DirectoryService directoryService ) throws LdapException
120    {
121        super.init( directoryService );
122
123        LOG.debug( "CollectiveAttribute interceptor initialized" );
124    }
125
126
127    // ------------------------------------------------------------------------
128    // Interceptor Method Overrides
129    // ------------------------------------------------------------------------
130    /**
131     * {@inheritDoc}
132     */
133    @Override
134    public void add( AddOperationContext addContext ) throws LdapException
135    {
136        checkAdd( addContext.getDn(), addContext.getEntry() );
137
138        next( addContext );
139    }
140
141
142    /**
143     * {@inheritDoc}
144     */
145    @Override
146    public Entry lookup( LookupOperationContext lookupContext ) throws LdapException
147    {
148        Entry result = next( lookupContext );
149
150        // do not add collective attributes
151        if ( lookupContext.isSyncreplLookup() )
152        {
153            return result;
154        }
155
156        // Adding the collective attributes if any
157        addCollectiveAttributes( lookupContext, result );
158
159        return result;
160    }
161
162
163    /**
164     * {@inheritDoc}
165     */
166    @Override
167    public void modify( ModifyOperationContext modifyContext ) throws LdapException
168    {
169        checkModify( modifyContext );
170
171        next( modifyContext );
172    }
173
174
175    /**
176     * {@inheritDoc}
177     */
178    @Override
179    public EntryFilteringCursor search( SearchOperationContext searchContext ) throws LdapException
180    {
181        EntryFilteringCursor cursor = next( searchContext );
182
183        // only add collective attributes for non-syncrepl search
184        if ( !searchContext.isSyncreplSearch() )
185        {
186            cursor.addEntryFilter( searchFilter );
187        }
188
189        return cursor;
190    }
191
192
193    //-------------------------------------------------------------------------------------
194    // Helper methods
195    //-------------------------------------------------------------------------------------
196    /**
197     * Check if we can add an entry. There are two cases : <br>
198     * <ul>
199     * <li>The entry is a normal entry : it should not contain any 'c-XXX' attributeType</li>
200     * <li>The entry is a collectiveAttributeSubentry
201     * </ul>
202     */
203    private void checkAdd( Dn normName, Entry entry ) throws LdapException
204    {
205        if ( entry.hasObjectClass( SchemaConstants.COLLECTIVE_ATTRIBUTE_SUBENTRY_OC ) )
206        {
207            // This is a collectiveAttribute subentry. It must have at least one collective
208            // attribute
209            for ( Attribute attribute : entry )
210            {
211                if ( attribute.getAttributeType().isCollective() )
212                {
213                    return;
214                }
215            }
216
217            LOG.info( "A CollectiveAttribute subentry *should* have at least one collectiveAttribute" );
218            throw new LdapSchemaViolationException( ResultCodeEnum.OBJECT_CLASS_VIOLATION,
219                I18n.err( I18n.ERR_257_COLLECTIVE_SUBENTRY_WITHOUT_COLLECTIVE_AT ) );
220        }
221
222        if ( containsAnyCollectiveAttributes( entry ) )
223        {
224            /*
225             * TODO: Replace the Exception and the ResultCodeEnum with the correct ones.
226             */
227            LOG.info(
228                "Cannot add the entry {} : it contains some CollectiveAttributes and is not a collective subentry",
229                entry );
230            throw new LdapSchemaViolationException( ResultCodeEnum.OBJECT_CLASS_VIOLATION,
231                I18n.err( I18n.ERR_241_CANNOT_STORE_COLLECTIVE_ATT_IN_ENTRY ) );
232        }
233    }
234
235
236    /**
237     * Check that we can modify an entry
238     */
239    private void checkModify( ModifyOperationContext modifyContext ) throws LdapException
240    {
241        List<Modification> mods = modifyContext.getModItems();
242        Entry originalEntry = modifyContext.getEntry();
243        Entry targetEntry = SchemaUtils.getTargetEntry( mods, originalEntry );
244
245        // If the modified entry contains the CollectiveAttributeSubentry, then the modification
246        // is accepted, no matter what
247        if ( targetEntry.contains( directoryService.getAtProvider().getObjectClass(),
248            SchemaConstants.COLLECTIVE_ATTRIBUTE_SUBENTRY_OC ) )
249        {
250            return;
251        }
252
253        // Check that we don't add any collectve attribute, this is not allowed on normal entries
254        if ( hasCollectiveAttributes( mods ) )
255        {
256            /*
257             * TODO: Replace the Exception and the ResultCodeEnum with the correct ones.
258             */
259            LOG.info(
260                "Cannot modify the entry {} : it contains some CollectiveAttributes and is not a collective subentry",
261                targetEntry );
262            throw new LdapSchemaViolationException( ResultCodeEnum.OBJECT_CLASS_VIOLATION, I18n.err( I18n.ERR_242 ) );
263        }
264    }
265
266
267    /**
268     * Check that we have a CollectiveAttribute in the modifications. (CollectiveAttributes
269     * are those with a name starting with 'c-').
270     */
271    private boolean hasCollectiveAttributes( List<Modification> mods ) throws LdapException
272    {
273        for ( Modification mod : mods )
274        {
275            // TODO: handle http://issues.apache.org/jira/browse/DIRSERVER-1198
276            Attribute attr = mod.getAttribute();
277            AttributeType attrType = attr.getAttributeType();
278
279            // Defensive programming. Very unlikely to happen here...
280            if ( attrType == null )
281            {
282                try
283                {
284                    attrType = schemaManager.lookupAttributeTypeRegistry( attr.getUpId() );
285                }
286                catch ( LdapException le )
287                {
288                    throw new LdapInvalidAttributeTypeException();
289                }
290            }
291
292            ModificationOperation modOp = mod.getOperation();
293
294            // If the AT is collective and we don't try to remove it, then we can return.
295            if ( attrType.isCollective() && ( modOp != ModificationOperation.REMOVE_ATTRIBUTE ) )
296            {
297                return true;
298            }
299        }
300
301        // No collective attrbute found
302        return false;
303    }
304
305
306    /**
307     * Check if the entry contains any collective AttributeType (those starting with 'c-')
308     */
309    private boolean containsAnyCollectiveAttributes( Entry entry )
310    {
311        for ( Attribute attribute : entry.getAttributes() )
312        {
313            AttributeType attributeType = attribute.getAttributeType();
314
315            if ( attributeType.isCollective() )
316            {
317                return true;
318            }
319        }
320
321        return false;
322    }
323
324
325    /**
326     * Adds the set of collective attributes requested in the returning attribute list
327     * and contained in subentries referenced by the entry. Excludes collective
328     * attributes that are specified to be excluded via the 'collectiveExclusions'
329     * attribute in the entry.
330     *
331     * @param opContext the context of the operation collective attributes
332     * are added to
333     * @param entry the entry to have the collective attributes injected
334     * @throws LdapException if there are problems accessing subentries
335     */
336    private void addCollectiveAttributes( FilteringOperationContext opContext, Entry entry )
337        throws LdapException
338    {
339        CoreSession session = opContext.getSession();
340
341        Attribute collectiveAttributeSubentries = ( ( ClonedServerEntry ) entry ).getOriginalEntry().get(
342            directoryService.getAtProvider().getCollectiveAttributeSubentries() );
343
344        /*
345         * If there are no collective attribute subentries referenced then we
346         * have no collective attributes to inject to this entry.
347         */
348        if ( collectiveAttributeSubentries == null )
349        {
350            return;
351        }
352
353        if ( LOG.isDebugEnabled() )
354        {
355            LOG.debug( "Filtering entry {}", entry.getDn() );
356        }
357
358        /*
359         * Before we proceed we need to lookup the exclusions within the entry
360         * and build a set of exclusions for rapid lookup.  We use OID values
361         * in the exclusions set instead of regular names that may have case
362         * variance.
363         */
364        Attribute collectiveExclusions = ( ( ClonedServerEntry ) entry ).getOriginalEntry().get(
365            directoryService.getAtProvider().getCollectiveExclusions() );
366        Set<AttributeType> exclusions = new HashSet<>();
367
368        if ( collectiveExclusions != null )
369        {
370            LOG.debug( "The entry has some exclusions : {}", collectiveExclusions );
371
372            if ( collectiveExclusions.contains( SchemaConstants.EXCLUDE_ALL_COLLECTIVE_ATTRIBUTES_AT_OID )
373                || collectiveExclusions.contains( SchemaConstants.EXCLUDE_ALL_COLLECTIVE_ATTRIBUTES_AT ) )
374            {
375                /*
376                 * This entry does not allow any collective attributes
377                 * to be injected into itself.
378                 */
379                LOG.debug( "The entry excludes all the collectiveAttributes" );
380
381                return;
382            }
383
384            for ( Value value : collectiveExclusions )
385            {
386                AttributeType attrType = schemaManager.lookupAttributeTypeRegistry( value.getString() );
387                exclusions.add( attrType );
388                LOG.debug( "Adding {} in the list of excluded collectiveAttributes", attrType.getName() );
389            }
390        }
391
392        /*
393         * For each collective subentry referenced by the entry we lookup the
394         * attributes of the subentry and copy collective attributes from the
395         * subentry into the entry.
396         */
397        for ( Value value : collectiveAttributeSubentries )
398        {
399            String subentryDnStr = value.getString();
400            Dn subentryDn = dnFactory.create( subentryDnStr );
401
402            LOG.debug( "Applying subentries {}", subentryDn.getName() );
403
404            /*
405             * TODO - Instead of hitting disk here can't we leverage the
406             * SubentryService to get us cached sub-entries so we're not
407             * wasting time with a lookup here? It is ridiculous to waste
408             * time looking up this sub-entry.
409             */
410
411            LookupOperationContext lookupContext = new LookupOperationContext( session, subentryDn,
412                SchemaConstants.ALL_ATTRIBUTES_ARRAY );
413            lookupContext.setPartition( opContext.getPartition() );
414            lookupContext.setTransaction( opContext.getTransaction() );
415
416            Entry subentry = directoryService.getPartitionNexus().lookup( lookupContext );
417
418            //LOG.debug( "Fetched the subentry : {}", subentry.getDn().getName() );
419
420            for ( Attribute attribute : subentry.getAttributes() )
421            {
422                AttributeType attributeType = attribute.getAttributeType();
423
424                // Skip the attributes which are not collective
425                if ( !attributeType.isCollective() )
426                {
427                    //LOG.debug( "The {} subentry attribute is not collective", attributeType.getName() );
428                    continue;
429                }
430
431                /*
432                 * Skip the addition of this collective attribute if it is excluded
433                 * in the 'collectiveAttributes' attribute.
434                 */
435                if ( exclusions.contains( attributeType ) )
436                {
437                    LOG.debug( "The {} subentry attribute has been removed, it's in the exclusion list",
438                        attributeType.getName() );
439                    continue;
440                }
441
442                /*
443                 * If not all attributes or this collective attribute requested specifically
444                 * then bypass the inclusion process.
445                 */
446                if ( !opContext.isAllUserAttributes() && !opContext.contains( schemaManager, attributeType ) )
447                {
448                    LOG.debug( "The {} subentry attribute is not in the list of attributes to return",
449                        attributeType.getName() );
450                    continue;
451                }
452
453                Attribute subentryColAttr = subentry.get( attributeType );
454                Attribute entryColAttr = entry.get( attributeType );
455
456                /*
457                 * If entry does not have attribute for collective attribute then create it.
458                 */
459                if ( entryColAttr == null )
460                {
461                    entryColAttr = new DefaultAttribute( attributeType );
462                    entry.put( entryColAttr );
463                }
464
465                /*
466                 *  Add all the collective attribute values in the subentry
467                 *  to the currently processed collective attribute in the entry.
468                 */
469                for ( Value subentryColVal : subentryColAttr )
470                {
471                    LOG.debug( "Adding the {} collective attribute into the entry", subentryColAttr );
472                    entryColAttr.add( subentryColVal.getString() );
473                }
474            }
475        }
476    }
477}