1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.ldap.server.schema;
18
19
20 import java.util.Collections;
21 import java.util.HashSet;
22 import java.util.Iterator;
23 import java.util.Map;
24 import java.util.Set;
25
26 import javax.naming.Name;
27 import javax.naming.NamingEnumeration;
28 import javax.naming.NamingException;
29 import javax.naming.directory.*;
30
31 import org.apache.ldap.common.filter.ExprNode;
32 import org.apache.ldap.common.filter.PresenceNode;
33 import org.apache.ldap.common.filter.SimpleNode;
34 import org.apache.ldap.common.message.LockableAttributeImpl;
35 import org.apache.ldap.common.message.LockableAttributesImpl;
36 import org.apache.ldap.common.message.ResultCodeEnum;
37 import org.apache.ldap.common.name.LdapName;
38 import org.apache.ldap.common.schema.AttributeType;
39 import org.apache.ldap.common.schema.DITContentRule;
40 import org.apache.ldap.common.schema.DITStructureRule;
41 import org.apache.ldap.common.schema.MatchingRule;
42 import org.apache.ldap.common.schema.MatchingRuleUse;
43 import org.apache.ldap.common.schema.NameForm;
44 import org.apache.ldap.common.schema.ObjectClass;
45 import org.apache.ldap.common.schema.SchemaUtils;
46 import org.apache.ldap.common.schema.Syntax;
47 import org.apache.ldap.common.util.SingletonEnumeration;
48 import org.apache.ldap.common.util.DateUtils;
49 import org.apache.ldap.common.util.AttributeUtils;
50 import org.apache.ldap.common.exception.LdapSchemaViolationException;
51 import org.apache.ldap.common.exception.LdapInvalidAttributeIdentifierException;
52 import org.apache.ldap.common.exception.LdapNoSuchAttributeException;
53 import org.apache.ldap.server.DirectoryServiceConfiguration;
54 import org.apache.ldap.server.invocation.Invocation;
55 import org.apache.ldap.server.invocation.InvocationStack;
56 import org.apache.ldap.server.configuration.InterceptorConfiguration;
57 import org.apache.ldap.server.enumeration.SearchResultFilteringEnumeration;
58 import org.apache.ldap.server.enumeration.SearchResultFilter;
59 import org.apache.ldap.server.interceptor.BaseInterceptor;
60 import org.apache.ldap.server.interceptor.NextInterceptor;
61 import org.apache.ldap.server.partition.DirectoryPartitionNexus;
62 import org.slf4j.Logger;
63 import org.slf4j.LoggerFactory;
64
65
66 /***
67 * An {@link org.apache.ldap.server.interceptor.Interceptor} that manages and enforces schemas.
68 *
69 * @todo Better interceptor description required.
70 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
71 * @version $Rev: 326075 $, $Date: 2005-10-18 06:13:54 -0400 (Tue, 18 Oct 2005) $
72 */
73 public class SchemaService extends BaseInterceptor
74 {
75 private static final String[] EMPTY_STRING_ARRAY = new String[0];
76 private static final String BINARY_KEY = "java.naming.ldap.attributes.binary";
77
78 /*** The LoggerFactory used by this Interceptor */
79 private static Logger log = LoggerFactory.getLogger( SchemaService.class );
80
81 /***
82 * the root nexus to all database partitions
83 */
84 private DirectoryPartitionNexus nexus;
85
86 /***
87 * a binary attribute tranforming filter: String -> byte[]
88 */
89 private BinaryAttributeFilter binaryAttributeFilter;
90
91 /***
92 * the global schema object registries
93 */
94 private GlobalRegistries globalRegistries;
95
96 /***
97 * subschemaSubentry attribute's value from Root DSE
98 */
99 private String subentryDn;
100
101 /***
102 * The time when the server started up.
103 */
104 private String startUpTimeStamp;
105
106 /***
107 * Creates a schema service interceptor.
108 */
109 public SchemaService()
110 {
111 startUpTimeStamp = DateUtils.getGeneralizedTime();
112 }
113
114
115 public void init( DirectoryServiceConfiguration factoryCfg, InterceptorConfiguration cfg ) throws NamingException
116 {
117 this.nexus = factoryCfg.getPartitionNexus();
118 this.globalRegistries = factoryCfg.getGlobalRegistries();
119 binaryAttributeFilter = new BinaryAttributeFilter();
120
121
122 String subschemaSubentry = ( String ) nexus.getRootDSE().get( "subschemaSubentry" ).get();
123 subentryDn = new LdapName( subschemaSubentry ).toString().toLowerCase();
124 }
125
126
127 public void destroy()
128 {
129 }
130
131
132 public NamingEnumeration list( NextInterceptor nextInterceptor, Name base ) throws NamingException
133 {
134 NamingEnumeration e = nextInterceptor.list( base );
135 Invocation invocation = InvocationStack.getInstance().peek();
136 return new SearchResultFilteringEnumeration( e, new SearchControls(), invocation, binaryAttributeFilter );
137 }
138
139
140 public NamingEnumeration search( NextInterceptor nextInterceptor,
141 Name base, Map env, ExprNode filter,
142 SearchControls searchCtls ) throws NamingException
143 {
144
145 if ( !subentryDn.equals( base.toString() ) )
146 {
147 return nextInterceptor.search( base, env, filter, searchCtls );
148 }
149
150 if ( searchCtls.getSearchScope() == SearchControls.OBJECT_SCOPE &&
151 filter instanceof SimpleNode )
152 {
153 SimpleNode node = ( SimpleNode ) filter;
154
155 if ( node.getAttribute().equalsIgnoreCase( "objectClass" ) &&
156 node.getValue().equalsIgnoreCase( "subschema" ) &&
157 node.getAssertionType() == SimpleNode.EQUALITY
158 )
159 {
160
161 Attributes attrs = getSubschemaEntry( searchCtls.getReturningAttributes() );
162 SearchResult result = new SearchResult( base.toString(), null, attrs );
163 return new SingletonEnumeration( result );
164 }
165 }
166 else if ( searchCtls.getSearchScope() == SearchControls.OBJECT_SCOPE &&
167 filter instanceof PresenceNode )
168 {
169 PresenceNode node = ( PresenceNode ) filter;
170
171 if ( node.getAttribute().equalsIgnoreCase( "objectClass" ) )
172 {
173
174 Attributes attrs = getSubschemaEntry( searchCtls.getReturningAttributes() );
175 SearchResult result = new SearchResult( base.toString(), null, attrs );
176 return new SingletonEnumeration( result );
177 }
178 }
179
180 NamingEnumeration e = nextInterceptor.search( base, env, filter, searchCtls );
181
182 if ( searchCtls.getReturningAttributes() != null )
183 {
184 return e;
185 }
186
187 Invocation invocation = InvocationStack.getInstance().peek();
188 return new SearchResultFilteringEnumeration( e, searchCtls, invocation, binaryAttributeFilter );
189 }
190
191
192 private Attributes getSubschemaEntry( String[] ids ) throws NamingException
193 {
194 if ( ids == null )
195 {
196 ids = EMPTY_STRING_ARRAY;
197 }
198
199 Set set = new HashSet();
200 LockableAttributesImpl attrs = new LockableAttributesImpl();
201 LockableAttributeImpl attr;
202
203 for ( int ii = 0; ii < ids.length; ii++ )
204 {
205 set.add( ids[ii].toLowerCase() );
206 }
207
208
209
210 boolean returnAllOperationalAttributes = set.contains( "+" );
211
212 if ( returnAllOperationalAttributes || set.contains( "objectclasses" ) )
213 {
214 attr = new LockableAttributeImpl( attrs, "objectClasses" );
215 Iterator list = globalRegistries.getObjectClassRegistry().list();
216 while ( list.hasNext() )
217 {
218 ObjectClass oc = ( ObjectClass ) list.next();
219 attr.add( SchemaUtils.render( oc ).toString() );
220 }
221 attrs.put( attr );
222 }
223
224 if ( returnAllOperationalAttributes || set.contains( "attributetypes" ) )
225 {
226 attr = new LockableAttributeImpl( attrs, "attributeTypes" );
227 Iterator list = globalRegistries.getAttributeTypeRegistry().list();
228 while ( list.hasNext() )
229 {
230 AttributeType at = ( AttributeType ) list.next();
231 attr.add( SchemaUtils.render( at ).toString() );
232 }
233 attrs.put( attr );
234 }
235
236 if ( returnAllOperationalAttributes || set.contains( "matchingrules" ) )
237 {
238 attr = new LockableAttributeImpl( attrs, "matchingRules" );
239 Iterator list = globalRegistries.getMatchingRuleRegistry().list();
240 while ( list.hasNext() )
241 {
242 MatchingRule mr = ( MatchingRule ) list.next();
243 attr.add( SchemaUtils.render( mr ).toString() );
244 }
245 attrs.put( attr );
246 }
247
248 if ( returnAllOperationalAttributes || set.contains( "matchingruleuse" ) )
249 {
250 attr = new LockableAttributeImpl( attrs, "matchingRuleUse" );
251 Iterator list = globalRegistries.getMatchingRuleUseRegistry().list();
252 while ( list.hasNext() )
253 {
254 MatchingRuleUse mru = ( MatchingRuleUse ) list.next();
255 attr.add( SchemaUtils.render( mru ).toString() );
256 }
257 attrs.put( attr );
258 }
259
260 if ( returnAllOperationalAttributes || set.contains( "ldapsyntaxes" ) )
261 {
262 attr = new LockableAttributeImpl( attrs, "ldapSyntaxes" );
263 Iterator list = globalRegistries.getSyntaxRegistry().list();
264 while ( list.hasNext() )
265 {
266 Syntax syntax = ( Syntax ) list.next();
267 attr.add( SchemaUtils.render( syntax ).toString() );
268 }
269 attrs.put( attr );
270 }
271
272 if ( returnAllOperationalAttributes || set.contains( "ditcontentrules" ) )
273 {
274 attr = new LockableAttributeImpl( attrs, "dITContentRules" );
275 Iterator list = globalRegistries.getDitContentRuleRegistry().list();
276 while ( list.hasNext() )
277 {
278 DITContentRule dcr = ( DITContentRule ) list.next();
279 attr.add( SchemaUtils.render( dcr ).toString() );
280 }
281 attrs.put( attr );
282 }
283
284 if ( returnAllOperationalAttributes || set.contains( "ditstructurerules" ) )
285 {
286 attr = new LockableAttributeImpl( attrs, "dITStructureRules" );
287 Iterator list = globalRegistries.getDitStructureRuleRegistry().list();
288 while ( list.hasNext() )
289 {
290 DITStructureRule dsr = ( DITStructureRule ) list.next();
291 attr.add( SchemaUtils.render( dsr ).toString() );
292 }
293 attrs.put( attr );
294 }
295
296 if ( returnAllOperationalAttributes || set.contains( "nameforms" ) )
297 {
298 attr = new LockableAttributeImpl( attrs, "nameForms" );
299 Iterator list = globalRegistries.getNameFormRegistry().list();
300 while ( list.hasNext() )
301 {
302 NameForm nf = ( NameForm ) list.next();
303 attr.add( SchemaUtils.render( nf ).toString() );
304 }
305 attrs.put( attr );
306 }
307
308
309
310
311
312 if ( returnAllOperationalAttributes || set.contains( "createtimestamp" ) )
313 {
314 attr = new LockableAttributeImpl( attrs, "createTimestamp" );
315 attr.add( startUpTimeStamp );
316 attrs.put( attr );
317 }
318
319 if ( returnAllOperationalAttributes || set.contains( "modifytimestamp" ) )
320 {
321 attr = new LockableAttributeImpl( attrs, "modifyTimestamp" );
322 attr.add( startUpTimeStamp );
323 attrs.put( attr );
324 }
325
326 if ( returnAllOperationalAttributes || set.contains( "creatorsname" ) )
327 {
328 attr = new LockableAttributeImpl( attrs, "creatorsName" );
329 attr.add( DirectoryPartitionNexus.ADMIN_PRINCIPAL );
330 attrs.put( attr );
331 }
332
333 if ( returnAllOperationalAttributes || set.contains( "modifiersname" ) )
334 {
335 attr = new LockableAttributeImpl( attrs, "modifiersName" );
336 attr.add( DirectoryPartitionNexus.ADMIN_PRINCIPAL );
337 attrs.put( attr );
338 }
339
340 int minSetSize = 0;
341 if ( set.contains( "+" ) )
342 {
343 minSetSize++;
344 }
345 if ( set.contains( "*" ) )
346 {
347 minSetSize++;
348 }
349 if ( set.contains( "ref" ) )
350 {
351 minSetSize++;
352 }
353
354
355 if ( set.contains( "*" ) || set.contains( "objectclass" ) || set.size() == minSetSize )
356 {
357 attr = new LockableAttributeImpl( attrs, "objectClass" );
358 attr.add( "top" );
359 attr.add( "subschema" );
360 attrs.put( attr );
361 }
362
363
364 if ( set.contains( "*" ) || set.contains( "cn" ) || set.contains( "commonname" ) || set.size() == minSetSize )
365 {
366 attrs.put( "cn", "schema" );
367 }
368
369 return attrs;
370 }
371
372
373 public Attributes lookup( NextInterceptor nextInterceptor, Name name ) throws NamingException
374 {
375 Attributes result = nextInterceptor.lookup( name );
376 Invocation invocation = InvocationStack.getInstance().peek();
377 doFilter( invocation, result );
378 return result;
379 }
380
381
382 public Attributes lookup( NextInterceptor nextInterceptor, Name name, String[] attrIds ) throws NamingException
383 {
384 Attributes result = nextInterceptor.lookup( name, attrIds );
385 if ( result == null )
386 {
387 return null;
388 }
389
390 Invocation invocation = InvocationStack.getInstance().peek();
391 doFilter( invocation, result );
392 return result;
393 }
394
395
396 /***
397 * Checks to see if an attribute is required by as determined from an entry's
398 * set of objectClass attribute values.
399 *
400 * @param attrId the attribute to test if required by a set of objectClass values
401 * @param objectClass the objectClass values
402 * @return true if the objectClass values require the attribute, false otherwise
403 * @throws NamingException if the attribute is not recognized
404 */
405 private boolean isRequired( String attrId, Attribute objectClass ) throws NamingException
406 {
407 OidRegistry oidRegistry = globalRegistries.getOidRegistry();
408 ObjectClassRegistry registry = globalRegistries.getObjectClassRegistry();
409
410 if ( ! oidRegistry.hasOid( attrId ) )
411 {
412 return false;
413 }
414
415 String attrOid = oidRegistry.getOid( attrId );
416 for ( int ii = 0; ii < objectClass.size(); ii++ )
417 {
418 ObjectClass ocSpec = registry.lookup( ( String ) objectClass.get( ii ) );
419 AttributeType[] mustList = ocSpec.getMustList();
420 for ( int jj = 0; jj < mustList.length; jj++ )
421 {
422 if ( mustList[jj].getOid().equals( attrOid ) )
423 {
424 return true;
425 }
426 }
427 }
428
429 return false;
430 }
431
432
433 /***
434 * Checks to see if removing a set of attributes from an entry completely removes
435 * that attribute's values. If change has zero size then all attributes are
436 * presumed to be removed.
437 *
438 * @param change
439 * @param entry
440 * @return
441 * @throws NamingException
442 */
443 private boolean isCompleteRemoval( Attribute change, Attributes entry ) throws NamingException
444 {
445
446 if ( change.size() == 0 )
447 {
448 return true;
449 }
450
451
452
453
454
455 Attribute changedEntryAttr = entry.get( change.getID() );
456 for ( int jj = 0; jj < change.size(); jj++ )
457 {
458 changedEntryAttr.remove( change.get( jj ) );
459 }
460
461 return changedEntryAttr.size() == 0;
462 }
463
464
465 Attribute getResultantObjectClasses( int modOp, Attribute changes, Attribute existing ) throws NamingException
466 {
467 if ( changes == null && existing == null )
468 {
469 return new LockableAttributeImpl( "objectClass" );
470 }
471
472 if ( changes == null )
473 {
474 return existing;
475 }
476
477 if ( existing == null && modOp == DirContext.ADD_ATTRIBUTE )
478 {
479 return changes;
480 }
481 else if ( existing == null )
482 {
483 return new LockableAttributeImpl( "objectClasses" );
484 }
485
486 switch( modOp )
487 {
488 case( DirContext.ADD_ATTRIBUTE ):
489 return AttributeUtils.getUnion( existing, changes );
490 case( DirContext.REPLACE_ATTRIBUTE ):
491 return ( Attribute ) changes.clone();
492 case( DirContext.REMOVE_ATTRIBUTE ):
493 return AttributeUtils.getDifference( existing, changes );
494 default:
495 throw new InternalError( "" );
496 }
497 }
498
499
500 public void modify( NextInterceptor next, Name name, int modOp, Attributes mods ) throws NamingException
501 {
502 Attributes entry = nexus.lookup( name );
503 Attribute objectClass = getResultantObjectClasses( modOp, mods.get( "objectClass"), entry.get( "objectClass" ) );
504 ObjectClassRegistry ocRegistry = this.globalRegistries.getObjectClassRegistry();
505 AttributeTypeRegistry atRegistry = this.globalRegistries.getAttributeTypeRegistry();
506
507 NamingEnumeration changes = mods.getIDs();
508 while ( changes.hasMore() )
509 {
510 String id = ( String ) changes.next();
511 Attribute change = mods.get( id );
512
513 if ( ! atRegistry.hasAttributeType( change.getID() ) && ! objectClass.contains( "extensibleObject" ) )
514 {
515 throw new LdapInvalidAttributeIdentifierException( "unrecognized attributeID " + change.getID() );
516 }
517
518 if ( modOp == DirContext.REMOVE_ATTRIBUTE && entry.get( change.getID() ) == null )
519 {
520 throw new LdapNoSuchAttributeException();
521 }
522
523
524
525 if ( modOp == DirContext.REMOVE_ATTRIBUTE &&
526 isRequired( change.getID(), objectClass ) &&
527 isCompleteRemoval( change, entry ) )
528 {
529 throw new LdapSchemaViolationException( ResultCodeEnum.OBJECTCLASSVIOLATION );
530 }
531 }
532
533 if ( modOp == DirContext.REMOVE_ATTRIBUTE )
534 {
535 SchemaChecker.preventRdnChangeOnModifyRemove( name, modOp, mods );
536 SchemaChecker.preventStructuralClassRemovalOnModifyRemove( ocRegistry, name, modOp, mods, objectClass );
537 }
538
539 if ( modOp == DirContext.REPLACE_ATTRIBUTE )
540 {
541 SchemaChecker.preventRdnChangeOnModifyReplace( name, modOp, mods );
542 SchemaChecker.preventStructuralClassRemovalOnModifyReplace( ocRegistry, name, modOp, mods );
543 }
544
545 next.modify( name, modOp, mods );
546 }
547
548
549 public void modify( NextInterceptor next, Name name, ModificationItem[] mods ) throws NamingException
550 {
551 Attributes entry = nexus.lookup( name );
552
553 ModificationItem objectClassMod = null;
554 for ( int ii = 0; ii < mods.length; ii++ )
555 {
556 if ( mods[ii].getAttribute().getID().equalsIgnoreCase( "objectclass" ) )
557 {
558 objectClassMod = mods[ii];
559 }
560 }
561 Attribute objectClass;
562
563 if ( objectClassMod == null )
564 {
565 objectClass = entry.get( "objectClass" );
566 }
567 else
568 {
569 objectClass = getResultantObjectClasses( objectClassMod.getModificationOp(),
570 objectClassMod.getAttribute(), entry.get( "objectClass" ) );
571 }
572
573 ObjectClassRegistry ocRegistry = this.globalRegistries.getObjectClassRegistry();
574 AttributeTypeRegistry atRegistry = this.globalRegistries.getAttributeTypeRegistry();
575
576 for ( int ii = 0; ii < mods.length; ii++ )
577 {
578 int modOp = mods[ii].getModificationOp();
579 Attribute change = mods[ii].getAttribute();
580
581 if ( ! atRegistry.hasAttributeType( change.getID() ) && ! objectClass.contains( "extensibleObject" ) )
582 {
583 throw new LdapInvalidAttributeIdentifierException();
584 }
585
586 if ( modOp == DirContext.REMOVE_ATTRIBUTE && entry.get( change.getID() ) == null )
587 {
588 throw new LdapNoSuchAttributeException();
589 }
590
591 if ( modOp == DirContext.REMOVE_ATTRIBUTE )
592 {
593
594
595 if ( isRequired( change.getID(), objectClass ) && isCompleteRemoval( change, entry ) )
596 {
597 throw new LdapSchemaViolationException( ResultCodeEnum.OBJECTCLASSVIOLATION );
598 }
599 SchemaChecker.preventRdnChangeOnModifyRemove( name, modOp, change );
600 SchemaChecker.preventStructuralClassRemovalOnModifyRemove( ocRegistry, name, modOp, change, objectClass );
601 }
602
603 if ( modOp == DirContext.REPLACE_ATTRIBUTE )
604 {
605 SchemaChecker.preventRdnChangeOnModifyReplace( name, modOp, change );
606 SchemaChecker.preventStructuralClassRemovalOnModifyReplace( ocRegistry, name, modOp, change );
607 }
608 }
609
610 next.modify( name, mods );
611 }
612
613
614 private void doFilter( Invocation invocation, Attributes entry )
615 throws NamingException
616 {
617
618 Set binaries;
619
620
621 String binaryIds = ( String ) invocation.getCaller().getEnvironment().get( BINARY_KEY );
622
623 if ( binaryIds == null )
624 {
625 binaries = Collections.EMPTY_SET;
626 }
627 else
628 {
629 String[] binaryArray = binaryIds.split( " " );
630
631 binaries = new HashSet( binaryArray.length );
632
633 for ( int ii = 0; ii < binaryArray.length; ii++ )
634 {
635 AttributeType type = globalRegistries.getAttributeTypeRegistry().lookup( binaryArray[ii] );
636
637 binaries.add( type );
638 }
639 }
640
641
642
643
644
645 NamingEnumeration list = entry.getIDs();
646
647 while ( list.hasMore() )
648 {
649 String id = ( String ) list.next();
650
651 AttributeType type = null;
652
653 boolean asBinary = false;
654
655 if ( globalRegistries.getAttributeTypeRegistry().hasAttributeType( id ) )
656 {
657 type = globalRegistries.getAttributeTypeRegistry().lookup( id );
658 }
659
660 if ( type != null )
661 {
662 asBinary = !type.getSyntax().isHumanReadible();
663
664 asBinary = asBinary || binaries.contains( type );
665 }
666
667 if ( asBinary )
668 {
669 Attribute attribute = entry.get( id );
670
671 Attribute binary = new LockableAttributeImpl( id );
672
673 for ( int ii = 0; ii < attribute.size(); ii++ )
674 {
675 Object value = attribute.get( ii );
676
677 if ( value instanceof String )
678 {
679 binary.add( ii, ( ( String ) value ).getBytes() );
680 }
681 else
682 {
683 binary.add( ii, value );
684 }
685 }
686
687 entry.remove( id );
688
689 entry.put( binary );
690 }
691 }
692 }
693
694
695 /***
696 * A special filter over entry attributes which replaces Attribute String values with their respective byte[]
697 * representations using schema information and the value held in the JNDI environment property:
698 * <code>java.naming.ldap.attributes.binary</code>.
699 *
700 * @see <a href= "http://java.sun.com/j2se/1.4.2/docs/guide/jndi/jndi-ldap-gl.html#binary">
701 * java.naming.ldap.attributes.binary</a>
702 */
703 private class BinaryAttributeFilter implements SearchResultFilter
704 {
705 public BinaryAttributeFilter()
706 {
707 }
708
709
710 public boolean accept( Invocation invocation, SearchResult result, SearchControls controls ) throws NamingException
711 {
712 doFilter( invocation, result.getAttributes() );
713 return true;
714 }
715 }
716 }