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.operational;
021
022
023 import java.util.HashSet;
024 import java.util.Iterator;
025 import java.util.List;
026 import java.util.Set;
027 import java.util.UUID;
028
029 import org.apache.directory.server.constants.ApacheSchemaConstants;
030 import org.apache.directory.server.constants.ServerDNConstants;
031 import org.apache.directory.server.core.DirectoryService;
032 import org.apache.directory.server.core.entry.ClonedServerEntry;
033 import org.apache.directory.server.core.filtering.EntryFilter;
034 import org.apache.directory.server.core.filtering.EntryFilteringCursor;
035 import org.apache.directory.server.core.interceptor.BaseInterceptor;
036 import org.apache.directory.server.core.interceptor.Interceptor;
037 import org.apache.directory.server.core.interceptor.NextInterceptor;
038 import org.apache.directory.server.core.interceptor.context.AddOperationContext;
039 import org.apache.directory.server.core.interceptor.context.ListOperationContext;
040 import org.apache.directory.server.core.interceptor.context.LookupOperationContext;
041 import org.apache.directory.server.core.interceptor.context.ModifyOperationContext;
042 import org.apache.directory.server.core.interceptor.context.MoveAndRenameOperationContext;
043 import org.apache.directory.server.core.interceptor.context.MoveOperationContext;
044 import org.apache.directory.server.core.interceptor.context.RenameOperationContext;
045 import org.apache.directory.server.core.interceptor.context.SearchOperationContext;
046 import org.apache.directory.server.core.interceptor.context.SearchingOperationContext;
047 import org.apache.directory.server.i18n.I18n;
048 import org.apache.directory.shared.ldap.constants.SchemaConstants;
049 import org.apache.directory.shared.ldap.entry.DefaultServerAttribute;
050 import org.apache.directory.shared.ldap.entry.DefaultServerEntry;
051 import org.apache.directory.shared.ldap.entry.EntryAttribute;
052 import org.apache.directory.shared.ldap.entry.Modification;
053 import org.apache.directory.shared.ldap.entry.ModificationOperation;
054 import org.apache.directory.shared.ldap.entry.ServerEntry;
055 import org.apache.directory.shared.ldap.entry.ServerModification;
056 import org.apache.directory.shared.ldap.entry.Value;
057 import org.apache.directory.shared.ldap.exception.LdapSchemaViolationException;
058 import org.apache.directory.shared.ldap.message.ResultCodeEnum;
059 import org.apache.directory.shared.ldap.name.AVA;
060 import org.apache.directory.shared.ldap.name.DN;
061 import org.apache.directory.shared.ldap.name.RDN;
062 import org.apache.directory.shared.ldap.schema.AttributeType;
063 import org.apache.directory.shared.ldap.schema.SchemaManager;
064 import org.apache.directory.shared.ldap.schema.UsageEnum;
065 import org.apache.directory.shared.ldap.util.DateUtils;
066 import org.slf4j.Logger;
067 import org.slf4j.LoggerFactory;
068
069
070 /**
071 * An {@link Interceptor} that adds or modifies the default attributes
072 * of entries. There are four default attributes for now;
073 * <tt>'creatorsName'</tt>, <tt>'createTimestamp'</tt>, <tt>'modifiersName'</tt>,
074 * and <tt>'modifyTimestamp'</tt>.
075 *
076 * @org.apache.xbean.XBean
077 *
078 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
079 * @version $Rev: 927839 $, $Date: 2010-03-26 15:25:10 +0200 (Fri, 26 Mar 2010) $
080 */
081 public class OperationalAttributeInterceptor extends BaseInterceptor
082 {
083 /** The LoggerFactory used by this Interceptor */
084 private static Logger LOG = LoggerFactory.getLogger( OperationalAttributeInterceptor.class );
085
086 private final EntryFilter DENORMALIZING_SEARCH_FILTER = new EntryFilter()
087 {
088 public boolean accept( SearchingOperationContext operation, ClonedServerEntry serverEntry )
089 throws Exception
090 {
091 if ( operation.getSearchControls().getReturningAttributes() == null )
092 {
093 return true;
094 }
095
096 return filterDenormalized( serverEntry );
097 }
098 };
099
100 /**
101 * the database search result filter to register with filter service
102 */
103 private final EntryFilter SEARCH_FILTER = new EntryFilter()
104 {
105 public boolean accept( SearchingOperationContext operation, ClonedServerEntry entry )
106 throws Exception
107 {
108 return operation.getSearchControls().getReturningAttributes() != null
109 || filterOperationalAttributes( entry );
110 }
111 };
112
113
114 private DirectoryService service;
115
116 private DN subschemaSubentryDn;
117
118 /** The schemaManager */
119 private SchemaManager schemaManager;
120
121 private static AttributeType CREATE_TIMESTAMP_ATTRIBUTE_TYPE;
122 private static AttributeType MODIFIERS_NAME_ATTRIBUTE_TYPE;
123 private static AttributeType MODIFY_TIMESTAMP_ATTRIBUTE_TYPE;
124
125
126 /**
127 * Creates the operational attribute management service interceptor.
128 */
129 public OperationalAttributeInterceptor()
130 {
131 }
132
133
134 public void init( DirectoryService directoryService ) throws Exception
135 {
136 service = directoryService;
137 schemaManager = directoryService.getSchemaManager();
138
139 // stuff for dealing with subentries (garbage for now)
140 Value<?> subschemaSubentry = service.getPartitionNexus()
141 .getRootDSE( null ).get( SchemaConstants.SUBSCHEMA_SUBENTRY_AT ).get();
142 subschemaSubentryDn = new DN( subschemaSubentry.getString() );
143 subschemaSubentryDn.normalize( schemaManager.getNormalizerMapping() );
144
145 CREATE_TIMESTAMP_ATTRIBUTE_TYPE = schemaManager.lookupAttributeTypeRegistry( SchemaConstants.CREATE_TIMESTAMP_AT );
146 MODIFIERS_NAME_ATTRIBUTE_TYPE = schemaManager.lookupAttributeTypeRegistry( SchemaConstants.MODIFIERS_NAME_AT );
147 MODIFY_TIMESTAMP_ATTRIBUTE_TYPE = schemaManager.lookupAttributeTypeRegistry( SchemaConstants.MODIFY_TIMESTAMP_AT );
148 }
149
150
151 public void destroy()
152 {
153 }
154
155
156 /**
157 * Adds extra operational attributes to the entry before it is added.
158 *
159 * We add those attributes :
160 * - creatorsName
161 * - createTimestamp
162 * - entryCSN
163 * - entryUUID
164 */
165 public void add( NextInterceptor nextInterceptor, AddOperationContext opContext )
166 throws Exception
167 {
168 String principal = getPrincipal().getName();
169
170 ServerEntry entry = opContext.getEntry();
171
172 /*
173 * @TODO : This code was probably created while working on Mitosis. Most probably dead code. Commented.
174 * Check JIRA DIRSERVER-1416
175 if ( opContext.getEntry().containsAttribute( CREATE_TIMESTAMP_ATTRIBUTE_TYPE ) )
176 {
177 // As we already have a CreateTimeStamp value in the context, use it, but only if
178 // the principal is admin
179 if ( opContext.getSession().getAuthenticatedPrincipal().getName().equals(
180 ServerDNConstants.ADMIN_SYSTEM_DN_NORMALIZED ))
181 {
182 entry.put( SchemaConstants.CREATE_TIMESTAMP_AT, DateUtils.getGeneralizedTime() );
183 }
184 else
185 {
186 String message = "The CreateTimeStamp attribute cannot be created by a user";
187 LOG.error( message );
188 throw new LdapSchemaViolationException( message, ResultCodeEnum.INSUFFICIENT_ACCESS_RIGHTS );
189 }
190 }
191 else
192 {
193 entry.put( SchemaConstants.CREATE_TIMESTAMP_AT, DateUtils.getGeneralizedTime() );
194 }
195 */
196
197 // Add the UUID and the entryCSN. The UUID is stored as a byte[] representation of
198 // its String value
199 // @TODO : If we are using replication, those four OAs may be already present.
200 // We have to deal with this as soon as we have the replication working again
201
202 // Check that we don't have an entryUUID AT in the incoming entry, as it's a NO-USER-MODIFICATION AT
203 // Of course, we will allow if for replication (see above @TODO)
204 boolean isAdmin = opContext.getSession().getAuthenticatedPrincipal().getName().equals(
205 ServerDNConstants.ADMIN_SYSTEM_DN_NORMALIZED );
206
207 if ( entry.containsAttribute( SchemaConstants.ENTRY_UUID_AT ) )
208 {
209 if ( !isAdmin )
210 {
211 // Wrong !
212 String message = I18n.err( I18n.ERR_30, SchemaConstants.ENTRY_UUID_AT );
213 LOG.error( message );
214 throw new LdapSchemaViolationException( ResultCodeEnum.INSUFFICIENT_ACCESS_RIGHTS, message );
215 }
216 }
217 else
218 {
219 entry.put( SchemaConstants.ENTRY_UUID_AT, UUID.randomUUID().toString() );
220 }
221
222 if ( entry.containsAttribute( SchemaConstants.ENTRY_CSN_AT ) )
223 {
224 if ( !isAdmin )
225 {
226 // Wrong !
227 String message = I18n.err( I18n.ERR_30, SchemaConstants.ENTRY_CSN_AT );
228 LOG.error( message );
229 throw new LdapSchemaViolationException( ResultCodeEnum.INSUFFICIENT_ACCESS_RIGHTS, message );
230 }
231 }
232 else
233 {
234 entry.put( SchemaConstants.ENTRY_CSN_AT, service.getCSN().toString() );
235 }
236
237 entry.put( SchemaConstants.CREATORS_NAME_AT, principal );
238 entry.put( SchemaConstants.CREATE_TIMESTAMP_AT, DateUtils.getGeneralizedTime() );
239
240 nextInterceptor.add( opContext );
241 }
242
243
244 public void modify( NextInterceptor nextInterceptor, ModifyOperationContext opContext )
245 throws Exception
246 {
247 // We must check that the user hasn't injected either the modifiersName
248 // or the modifyTimestamp operational attributes : they are not supposed to be
249 // added at this point.
250 // If so, remove them, and if there are no more attributes, simply return.
251 // otherwise, inject those values into the list of modifications
252 List<Modification> mods = opContext.getModItems();
253
254 for ( Modification modification: mods )
255 {
256 AttributeType attributeType = modification.getAttribute().getAttributeType();
257
258 if ( attributeType.equals( MODIFIERS_NAME_ATTRIBUTE_TYPE ) )
259 {
260 String message = I18n.err( I18n.ERR_31 );
261 LOG.error( message );
262 throw new LdapSchemaViolationException( ResultCodeEnum.INSUFFICIENT_ACCESS_RIGHTS, message );
263 }
264
265 if ( attributeType.equals( MODIFY_TIMESTAMP_ATTRIBUTE_TYPE ) )
266 {
267 String message = I18n.err( I18n.ERR_32 );
268 LOG.error( message );
269 throw new LdapSchemaViolationException( ResultCodeEnum.INSUFFICIENT_ACCESS_RIGHTS, message );
270 }
271 }
272
273 // Inject the ModifiersName AT if it's not present
274 EntryAttribute attribute = new DefaultServerAttribute(
275 MODIFIERS_NAME_ATTRIBUTE_TYPE,
276 getPrincipal().getName());
277
278 Modification modifiersName = new ServerModification( ModificationOperation.REPLACE_ATTRIBUTE, attribute );
279
280 mods.add( modifiersName );
281
282 // Inject the ModifyTimestamp AT if it's not present
283 attribute = new DefaultServerAttribute(
284 MODIFY_TIMESTAMP_ATTRIBUTE_TYPE,
285 DateUtils.getGeneralizedTime() );
286
287 Modification timestamp = new ServerModification( ModificationOperation.REPLACE_ATTRIBUTE, attribute );
288
289 mods.add( timestamp );
290
291 // Go down in the chain
292 nextInterceptor.modify( opContext );
293
294 if ( opContext.getDn().getNormName().equals( subschemaSubentryDn.getNormName() ) )
295 {
296 return;
297 }
298
299 // -------------------------------------------------------------------
300 // Add the operational attributes for the modifier first
301 // -------------------------------------------------------------------
302 // TODO : Why can't we add those elements on teh original modifications ???
303 // Or into the context ?
304 /*
305 List<Modification> modItemList = new ArrayList<Modification>(2);
306
307 AttributeType modifiersNameAt = atRegistry.lookup( SchemaConstants.MODIFIERS_NAME_AT );
308 ServerAttribute attribute = new DefaultServerAttribute(
309 modifiersNameAt,
310 getPrincipal().getName());
311
312 Modification modifiers = new ServerModification( ModificationOperation.REPLACE_ATTRIBUTE, attribute );
313 modItemList.add( modifiers );
314
315 AttributeType modifyTimeStampAt = atRegistry.lookup( SchemaConstants.MODIFY_TIMESTAMP_AT );
316 attribute = new DefaultServerAttribute(
317 modifyTimeStampAt,
318 DateUtils.getGeneralizedTime() );
319
320 Modification timestamp = new ServerModification( ModificationOperation.REPLACE_ATTRIBUTE, attribute );
321 modItemList.add( timestamp );
322
323 // -------------------------------------------------------------------
324 // Make the modify() call happen
325 // -------------------------------------------------------------------
326 ModifyOperationContext newModify = new ModifyOperationContext( opContext.getSession(),
327 opContext.getDn(), modItemList );
328 newModify.setEntry( opContext.getAlteredEntry() );
329 service.getPartitionNexus().modify( newModify );
330 */
331 }
332
333
334 public void rename( NextInterceptor nextInterceptor, RenameOperationContext opContext )
335 throws Exception
336 {
337 nextInterceptor.rename( opContext );
338
339 DN newDn = opContext.getNewDn();
340
341 // add operational attributes after call in case the operation fails
342 ServerEntry serverEntry = new DefaultServerEntry( schemaManager, newDn );
343 serverEntry.put( SchemaConstants.MODIFIERS_NAME_AT, getPrincipal().getName() );
344 serverEntry.put( SchemaConstants.MODIFY_TIMESTAMP_AT, DateUtils.getGeneralizedTime() );
345
346 List<Modification> items = ModifyOperationContext.createModItems( serverEntry, ModificationOperation.REPLACE_ATTRIBUTE );
347
348 ModifyOperationContext newModify = new ModifyOperationContext( opContext.getSession(), newDn, items );
349 newModify.setEntry( opContext.getAlteredEntry() );
350
351 service.getPartitionNexus().modify( newModify );
352 }
353
354
355 public void move( NextInterceptor nextInterceptor, MoveOperationContext opContext ) throws Exception
356 {
357 nextInterceptor.move( opContext );
358
359 // add operational attributes after call in case the operation fails
360 ServerEntry serverEntry = new DefaultServerEntry( schemaManager, opContext.getDn() );
361 serverEntry.put( SchemaConstants.MODIFIERS_NAME_AT, getPrincipal().getName() );
362 serverEntry.put( SchemaConstants.MODIFY_TIMESTAMP_AT, DateUtils.getGeneralizedTime() );
363
364 List<Modification> items = ModifyOperationContext.createModItems( serverEntry, ModificationOperation.REPLACE_ATTRIBUTE );
365
366
367 ModifyOperationContext newModify =
368 new ModifyOperationContext( opContext.getSession(), opContext.getParent(), items );
369
370 service.getPartitionNexus().modify( newModify );
371 }
372
373
374 public void moveAndRename( NextInterceptor nextInterceptor, MoveAndRenameOperationContext opContext )
375 throws Exception
376 {
377 nextInterceptor.moveAndRename( opContext );
378
379 // add operational attributes after call in case the operation fails
380 ServerEntry serverEntry = new DefaultServerEntry( schemaManager, opContext.getDn() );
381 serverEntry.put( SchemaConstants.MODIFIERS_NAME_AT, getPrincipal().getName() );
382 serverEntry.put( SchemaConstants.MODIFY_TIMESTAMP_AT, DateUtils.getGeneralizedTime() );
383
384 List<Modification> items = ModifyOperationContext.createModItems( serverEntry, ModificationOperation.REPLACE_ATTRIBUTE );
385
386 ModifyOperationContext newModify =
387 new ModifyOperationContext( opContext.getSession(), opContext.getParent(), items );
388
389 service.getPartitionNexus().modify( newModify );
390 }
391
392
393 public ClonedServerEntry lookup( NextInterceptor nextInterceptor, LookupOperationContext opContext ) throws Exception
394 {
395 ClonedServerEntry result = nextInterceptor.lookup( opContext );
396
397 if ( result == null )
398 {
399 return null;
400 }
401
402 if ( opContext.getAttrsId() == null )
403 {
404 filterOperationalAttributes( result );
405 }
406 else if ( ( opContext.getAllOperational() == null ) || ( opContext.getAllOperational() == false ) )
407 {
408 filter( opContext, result );
409 }
410
411 denormalizeEntryOpAttrs( result );
412 return result;
413 }
414
415
416 public EntryFilteringCursor list( NextInterceptor nextInterceptor, ListOperationContext opContext ) throws Exception
417 {
418 EntryFilteringCursor cursor = nextInterceptor.list( opContext );
419 cursor.addEntryFilter( SEARCH_FILTER );
420 return cursor;
421 }
422
423
424 public EntryFilteringCursor search( NextInterceptor nextInterceptor, SearchOperationContext opContext ) throws Exception
425 {
426 EntryFilteringCursor cursor = nextInterceptor.search( opContext );
427
428 if ( opContext.isAllOperationalAttributes() ||
429 ( opContext.getReturningAttributes() != null && ! opContext.getReturningAttributes().isEmpty() ) )
430 {
431 if ( service.isDenormalizeOpAttrsEnabled() )
432 {
433 cursor.addEntryFilter( DENORMALIZING_SEARCH_FILTER );
434 }
435
436 return cursor;
437 }
438
439 cursor.addEntryFilter( SEARCH_FILTER );
440 return cursor;
441 }
442
443
444 /**
445 * Filters out the operational attributes within a search results attributes. The attributes are directly
446 * modified.
447 *
448 * @param attributes the resultant attributes to filter
449 * @return true always
450 * @throws Exception if there are failures in evaluation
451 */
452 private boolean filterOperationalAttributes( ServerEntry attributes ) throws Exception
453 {
454 Set<AttributeType> removedAttributes = new HashSet<AttributeType>();
455
456 // Build a list of attributeType to remove
457 for ( AttributeType attributeType:attributes.getAttributeTypes() )
458 {
459 if ( attributeType.getUsage() != UsageEnum.USER_APPLICATIONS )
460 {
461 removedAttributes.add( attributeType );
462 }
463 }
464
465 // Now remove the attributes which are not USERs
466 for ( AttributeType attributeType:removedAttributes )
467 {
468 attributes.removeAttributes( attributeType );
469 }
470
471 return true;
472 }
473
474
475 private void filter( LookupOperationContext lookupContext, ServerEntry entry ) throws Exception
476 {
477 DN dn = lookupContext.getDn();
478 List<String> ids = lookupContext.getAttrsId();
479
480 // still need to protect against returning op attrs when ids is null
481 if ( ids == null || ids.isEmpty() )
482 {
483 filterOperationalAttributes( entry );
484 return;
485 }
486
487 Set<AttributeType> attributeTypes = entry.getAttributeTypes();
488
489 if ( dn.size() == 0 )
490 {
491 for ( AttributeType attributeType:attributeTypes )
492 {
493 if ( !ids.contains( attributeType.getOid() ) )
494 {
495 entry.removeAttributes( attributeType );
496 }
497 }
498 }
499
500 denormalizeEntryOpAttrs( entry );
501
502 // do nothing past here since this explicity specifies which
503 // attributes to include - backends will automatically populate
504 // with right set of attributes using ids array
505 }
506
507
508 public void denormalizeEntryOpAttrs( ServerEntry entry ) throws Exception
509 {
510 if ( service.isDenormalizeOpAttrsEnabled() )
511 {
512 EntryAttribute attr = entry.get( SchemaConstants.CREATORS_NAME_AT );
513
514 if ( attr != null )
515 {
516 DN creatorsName = new DN( attr.getString() );
517
518 attr.clear();
519 attr.add( denormalizeTypes( creatorsName ).getName() );
520 }
521
522 attr = entry.get( SchemaConstants.MODIFIERS_NAME_AT );
523
524 if ( attr != null )
525 {
526 DN modifiersName = new DN( attr.getString() );
527
528 attr.clear();
529 attr.add( denormalizeTypes( modifiersName ).getName() );
530 }
531
532 attr = entry.get( ApacheSchemaConstants.SCHEMA_MODIFIERS_NAME_AT );
533
534 if ( attr != null )
535 {
536 DN modifiersName = new DN( attr.getString() );
537
538 attr.clear();
539 attr.add( denormalizeTypes( modifiersName ).getName() );
540 }
541 }
542 }
543
544
545 /**
546 * Does not create a new DN but alters existing DN by using the first
547 * short name for an attributeType definition.
548 *
549 * @param dn the normalized distinguished name
550 * @return the distinuished name denormalized
551 * @throws Exception if there are problems denormalizing
552 */
553 public DN denormalizeTypes( DN dn ) throws Exception
554 {
555 DN newDn = new DN();
556
557 for ( int ii = 0; ii < dn.size(); ii++ )
558 {
559 RDN rdn = dn.getRdn( ii );
560 if ( rdn.size() == 0 )
561 {
562 newDn.add( new RDN() );
563 continue;
564 }
565 else if ( rdn.size() == 1 )
566 {
567 String name = schemaManager.lookupAttributeTypeRegistry( rdn.getNormType() ).getName();
568 String value = rdn.getAtav().getNormValue().getString();
569 newDn.add( new RDN( name, name, value, value ) );
570 continue;
571 }
572
573 // below we only process multi-valued rdns
574 StringBuffer buf = new StringBuffer();
575
576 for ( Iterator<AVA> atavs = rdn.iterator(); atavs.hasNext(); /**/ )
577 {
578 AVA atav = atavs.next();
579 String type = schemaManager.lookupAttributeTypeRegistry( rdn.getNormType() ).getName();
580 buf.append( type ).append( '=' ).append( atav.getNormValue() );
581
582 if ( atavs.hasNext() )
583 {
584 buf.append( '+' );
585 }
586 }
587
588 newDn.add( new RDN(buf.toString()) );
589 }
590
591 return newDn;
592 }
593
594
595 private boolean filterDenormalized( ServerEntry entry ) throws Exception
596 {
597 denormalizeEntryOpAttrs( entry );
598 return true;
599 }
600 }