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 */
019package org.apache.directory.server.core.changelog;
020
021
022import java.util.ArrayList;
023import java.util.List;
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.DefaultEntry;
028import org.apache.directory.api.ldap.model.entry.Entry;
029import org.apache.directory.api.ldap.model.entry.Modification;
030import org.apache.directory.api.ldap.model.exception.LdapException;
031import org.apache.directory.api.ldap.model.ldif.ChangeType;
032import org.apache.directory.api.ldap.model.ldif.LdifEntry;
033import org.apache.directory.api.ldap.model.ldif.LdifRevertor;
034import org.apache.directory.api.ldap.model.message.controls.ManageDsaITImpl;
035import org.apache.directory.api.ldap.model.name.Dn;
036import org.apache.directory.api.ldap.model.schema.AttributeType;
037import org.apache.directory.server.constants.ApacheSchemaConstants;
038import org.apache.directory.server.constants.ServerDNConstants;
039import org.apache.directory.server.core.api.CoreSession;
040import org.apache.directory.server.core.api.DirectoryService;
041import org.apache.directory.server.core.api.InterceptorEnum;
042import org.apache.directory.server.core.api.changelog.ChangeLog;
043import org.apache.directory.server.core.api.entry.ClonedServerEntry;
044import org.apache.directory.server.core.api.entry.ServerEntryUtils;
045import org.apache.directory.server.core.api.interceptor.BaseInterceptor;
046import org.apache.directory.server.core.api.interceptor.context.AddOperationContext;
047import org.apache.directory.server.core.api.interceptor.context.DeleteOperationContext;
048import org.apache.directory.server.core.api.interceptor.context.LookupOperationContext;
049import org.apache.directory.server.core.api.interceptor.context.ModifyOperationContext;
050import org.apache.directory.server.core.api.interceptor.context.MoveAndRenameOperationContext;
051import org.apache.directory.server.core.api.interceptor.context.MoveOperationContext;
052import org.apache.directory.server.core.api.interceptor.context.OperationContext;
053import org.apache.directory.server.core.api.interceptor.context.RenameOperationContext;
054import org.apache.directory.server.core.shared.SchemaService;
055import org.slf4j.Logger;
056import org.slf4j.LoggerFactory;
057
058
059/**
060 * An interceptor which intercepts write operations to the directory and
061 * logs them with the server's ChangeLog service.
062 * Note: Adding/deleting a tag is not recorded as a change
063 */
064public class ChangeLogInterceptor extends BaseInterceptor
065{
066    /** for debugging */
067    private static final Logger LOG = LoggerFactory.getLogger( ChangeLogInterceptor.class );
068
069    /** used to ignore modify operations to tombstone entries */
070    private AttributeType entryDeleted;
071
072    /** the changelog service to log changes to */
073    private ChangeLog changeLog;
074
075    /** OID of the 'rev' attribute used in changeLogEvent and tag objectclasses */
076    private static final String REV_AT_OID = "1.3.6.1.4.1.18060.0.4.1.2.47";
077
078
079    /**
080     * Creates a new instance of a ChangeLogInterceptor.
081     */
082    public ChangeLogInterceptor()
083    {
084        super( InterceptorEnum.CHANGE_LOG_INTERCEPTOR );
085    }
086
087
088    // -----------------------------------------------------------------------
089    // Overridden init() and destroy() methods
090    // -----------------------------------------------------------------------
091    /**
092     * The init method will initialize the local variables and load the
093     * entryDeleted AttributeType.
094     */
095    @Override
096    public void init( DirectoryService directoryService ) throws LdapException
097    {
098        super.init( directoryService );
099
100        changeLog = directoryService.getChangeLog();
101        entryDeleted = directoryService.getSchemaManager()
102            .getAttributeType( ApacheSchemaConstants.ENTRY_DELETED_AT_OID );
103    }
104
105
106    // -----------------------------------------------------------------------
107    // Overridden (only change inducing) intercepted methods
108    // -----------------------------------------------------------------------
109    /**
110     * {@inheritDoc}
111     */
112    @Override
113    public void add( AddOperationContext addContext ) throws LdapException
114    {
115        next( addContext );
116
117        if ( !changeLog.isEnabled() )
118        {
119            return;
120        }
121
122        Entry addEntry = addContext.getEntry();
123
124        // we don't want to record addition of a tag as a change
125        if ( addEntry.get( REV_AT_OID ) != null )
126        {
127            return;
128        }
129
130        LdifEntry forward = new LdifEntry();
131        forward.setChangeType( ChangeType.Add );
132        forward.setDn( addContext.getDn() );
133
134        for ( Attribute attribute : addEntry.getAttributes() )
135        {
136            AttributeType attributeType = attribute.getAttributeType();
137            forward.addAttribute( addEntry.get( attributeType ).clone() );
138        }
139
140        LdifEntry reverse = LdifRevertor.reverseAdd( addContext.getDn() );
141        addContext.setChangeLogEvent( changeLog.log( getPrincipal( addContext ), forward, reverse ) );
142    }
143
144
145    /**
146     * The delete operation has to be stored with a way to restore the deleted element.
147     * There is no way to do that but reading the entry and dump it into the LOG.
148     */
149    /**
150     * {@inheritDoc}
151     */
152    @Override
153    public void delete( DeleteOperationContext deleteContext ) throws LdapException
154    {
155        // @todo make sure we're not putting in operational attributes that cannot be user modified
156        // must save the entry if change log is enabled
157        Entry serverEntry = null;
158
159        if ( changeLog.isEnabled() )
160        {
161            serverEntry = getAttributes( deleteContext );
162        }
163
164        next( deleteContext );
165
166        if ( !changeLog.isEnabled() )
167        {
168            return;
169        }
170
171        // we don't want to record deleting a tag as a change
172        if ( serverEntry.get( REV_AT_OID ) != null )
173        {
174            return;
175        }
176
177        LdifEntry forward = new LdifEntry();
178        forward.setChangeType( ChangeType.Delete );
179        forward.setDn( deleteContext.getDn() );
180
181        Entry reverseEntry = new DefaultEntry( serverEntry.getDn() );
182
183        boolean isCollectiveSubentry = serverEntry.hasObjectClass( SchemaConstants.COLLECTIVE_ATTRIBUTE_SUBENTRY_OC );
184
185        for ( Attribute attribute : serverEntry )
186        {
187            // filter collective attributes, they can't be added by the revert operation
188            AttributeType at = schemaManager.lookupAttributeTypeRegistry( attribute.getId() );
189
190            if ( !at.isCollective() || isCollectiveSubentry )
191            {
192                reverseEntry.add( attribute.clone() );
193            }
194        }
195
196        LdifEntry reverse = LdifRevertor.reverseDel( deleteContext.getDn(), reverseEntry );
197        deleteContext.setChangeLogEvent( changeLog.log( getPrincipal( deleteContext ), forward, reverse ) );
198    }
199
200
201    /**
202     * {@inheritDoc}
203     */
204    @Override
205    public void modify( ModifyOperationContext modifyContext ) throws LdapException
206    {
207        Entry serverEntry = null;
208        Modification modification = ServerEntryUtils.getModificationItem( modifyContext.getModItems(), entryDeleted );
209        boolean isDelete = modification != null;
210
211        if ( !isDelete && ( changeLog.isEnabled() ) )
212        {
213            // @todo make sure we're not putting in operational attributes that cannot be user modified
214            serverEntry = getAttributes( modifyContext );
215        }
216
217        // Duplicate modifications so that the reverse does not contain the operational attributes
218        List<Modification> clonedMods = new ArrayList<>();
219
220        for ( Modification mod : modifyContext.getModItems() )
221        {
222            clonedMods.add( mod.clone() );
223        }
224
225        // Call the next interceptor
226        next( modifyContext );
227
228        // @TODO: needs big consideration!!!
229        // NOTE: perhaps we need to log this as a system operation that cannot and should not be reapplied?
230        if ( isDelete 
231            || !changeLog.isEnabled()
232
233            // if there are no modifications due to stripping out bogus non-
234            // existing attributes then we will have no modification items and
235            // should ignore not this without registering it with the changelog
236
237            || modifyContext.getModItems().isEmpty() )
238        {
239            if ( isDelete )
240            {
241                LOG.debug( "Bypassing changelog on modify of entryDeleted attribute." );
242            }
243
244            return;
245        }
246
247        LdifEntry forward = new LdifEntry();
248        forward.setChangeType( ChangeType.Modify );
249        forward.setDn( modifyContext.getDn() );
250
251        List<Modification> mods = new ArrayList<>( clonedMods.size() );
252
253        for ( Modification modItem : clonedMods )
254        {
255            // TODO: handle correctly http://issues.apache.org/jira/browse/DIRSERVER-1198
256            mods.add( modItem );
257
258            forward.addModification( modItem );
259        }
260
261        Entry clientEntry = new DefaultEntry( serverEntry.getDn() );
262
263        for ( Attribute attribute : serverEntry )
264        {
265            clientEntry.add( attribute.clone() );
266        }
267
268        LdifEntry reverse = LdifRevertor.reverseModify(
269            modifyContext.getDn(),
270            mods,
271            clientEntry );
272
273        modifyContext.setChangeLogEvent( changeLog.log( getPrincipal( modifyContext ), forward, reverse ) );
274    }
275
276
277    /**
278     * {@inheritDoc}
279     */
280    @Override
281    public void move( MoveOperationContext moveContext ) throws LdapException
282    {
283        next( moveContext );
284
285        if ( !changeLog.isEnabled() )
286        {
287            return;
288        }
289
290        LdifEntry forward = new LdifEntry();
291        forward.setChangeType( ChangeType.ModDn );
292        forward.setDn( moveContext.getDn() );
293        forward.setNewSuperior( moveContext.getNewSuperior().getName() );
294
295        LdifEntry reverse = LdifRevertor.reverseMove( moveContext.getNewSuperior(), moveContext.getDn() );
296        moveContext.setChangeLogEvent( changeLog.log( getPrincipal( moveContext ), forward, reverse ) );
297    }
298
299
300    /**
301     * {@inheritDoc}
302     */
303    @Override
304    public void moveAndRename( MoveAndRenameOperationContext moveAndRenameContext ) throws LdapException
305    {
306        Entry serverEntry = null;
307
308        if ( changeLog.isEnabled() )
309        {
310            // @todo make sure we're not putting in operational attributes that cannot be user modified
311            serverEntry = moveAndRenameContext.getOriginalEntry();
312        }
313
314        next( moveAndRenameContext );
315
316        if ( !changeLog.isEnabled() )
317        {
318            return;
319        }
320
321        LdifEntry forward = new LdifEntry();
322        forward.setChangeType( ChangeType.ModDn );
323        forward.setDn( moveAndRenameContext.getDn() );
324        forward.setDeleteOldRdn( moveAndRenameContext.getDeleteOldRdn() );
325        forward.setNewRdn( moveAndRenameContext.getNewRdn().getName() );
326        forward.setNewSuperior( moveAndRenameContext.getNewSuperiorDn().getName() );
327
328        List<LdifEntry> reverses = LdifRevertor.reverseMoveAndRename(
329            serverEntry, moveAndRenameContext.getNewSuperiorDn(), moveAndRenameContext.getNewRdn(), false );
330
331        if ( moveAndRenameContext.isReferralIgnored() )
332        {
333            forward.addControl( new ManageDsaITImpl() );
334            LdifEntry reversedEntry = reverses.get( 0 );
335            reversedEntry.addControl( new ManageDsaITImpl() );
336        }
337
338        moveAndRenameContext
339            .setChangeLogEvent( changeLog.log( getPrincipal( moveAndRenameContext ), forward, reverses ) );
340    }
341
342
343    /**
344     * {@inheritDoc}
345     */
346    @Override
347    public void rename( RenameOperationContext renameContext ) throws LdapException
348    {
349        Entry serverEntry = null;
350
351        if ( renameContext.getEntry() != null )
352        {
353            serverEntry = ( ( ClonedServerEntry ) renameContext.getEntry() ).getOriginalEntry();
354        }
355
356        next( renameContext );
357
358        // After this point, the entry has been modified. The cloned entry contains
359        // the modified entry, the originalEntry has changed
360
361        if ( !changeLog.isEnabled() )
362        {
363            return;
364        }
365
366        LdifEntry forward = new LdifEntry();
367        forward.setChangeType( ChangeType.ModRdn );
368        forward.setDn( renameContext.getDn() );
369        forward.setNewRdn( renameContext.getNewRdn().getName() );
370        forward.setDeleteOldRdn( renameContext.getDeleteOldRdn() );
371
372        List<LdifEntry> reverses = LdifRevertor.reverseRename(
373            serverEntry, renameContext.getNewRdn(), renameContext.getDeleteOldRdn() );
374
375        renameContext.setChangeLogEvent( changeLog.log( getPrincipal( renameContext ), forward, reverses ) );
376    }
377
378
379    /**
380     * Gets attributes required for modifications.
381     *
382     * @param dn the dn of the entry to get
383     * @return the entry's attributes (may be immutable if the schema subentry)
384     * @throws Exception on error accessing the entry's attributes
385     */
386    private Entry getAttributes( OperationContext opContext ) throws LdapException
387    {
388        Dn dn = opContext.getDn();
389        Entry serverEntry;
390
391        // @todo make sure we're not putting in operational attributes that cannot be user modified
392        if ( dn.equals( ServerDNConstants.CN_SCHEMA_DN ) )
393        {
394            return SchemaService.getSubschemaEntryCloned( directoryService );
395        }
396        else
397        {
398            CoreSession session = opContext.getSession();
399            LookupOperationContext lookupContext = new LookupOperationContext( session, dn, SchemaConstants.ALL_ATTRIBUTES_ARRAY );
400            lookupContext.setPartition( opContext.getPartition() );
401            lookupContext.setTransaction( opContext.getTransaction() );
402            
403            serverEntry = directoryService.getPartitionNexus().lookup( lookupContext );
404        }
405
406        return serverEntry;
407    }
408}