blob: d110c1e695daa0148bc2924632ea31cf07cb15d4 [file] [log] [blame]
/*
* Copyright 2004 The Apache Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.apache.ldap.server.schema;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import javax.naming.Name;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.*;
import org.apache.asn1.codec.util.StringUtils;
import org.apache.ldap.common.filter.ExprNode;
import org.apache.ldap.common.filter.PresenceNode;
import org.apache.ldap.common.filter.SimpleNode;
import org.apache.ldap.common.message.LockableAttributeImpl;
import org.apache.ldap.common.message.LockableAttributesImpl;
import org.apache.ldap.common.message.ResultCodeEnum;
import org.apache.ldap.common.name.LdapName;
import org.apache.ldap.common.schema.AttributeType;
import org.apache.ldap.common.schema.DITContentRule;
import org.apache.ldap.common.schema.DITStructureRule;
import org.apache.ldap.common.schema.MatchingRule;
import org.apache.ldap.common.schema.MatchingRuleUse;
import org.apache.ldap.common.schema.NameForm;
import org.apache.ldap.common.schema.ObjectClass;
import org.apache.ldap.common.schema.SchemaUtils;
import org.apache.ldap.common.schema.Syntax;
import org.apache.ldap.common.util.SingletonEnumeration;
import org.apache.ldap.common.util.DateUtils;
import org.apache.ldap.common.util.AttributeUtils;
import org.apache.ldap.common.exception.LdapSchemaViolationException;
import org.apache.ldap.common.exception.LdapInvalidAttributeIdentifierException;
import org.apache.ldap.common.exception.LdapNoSuchAttributeException;
import org.apache.ldap.server.DirectoryServiceConfiguration;
import org.apache.ldap.server.invocation.Invocation;
import org.apache.ldap.server.invocation.InvocationStack;
import org.apache.ldap.server.configuration.InterceptorConfiguration;
import org.apache.ldap.server.enumeration.SearchResultFilteringEnumeration;
import org.apache.ldap.server.enumeration.SearchResultFilter;
import org.apache.ldap.server.interceptor.BaseInterceptor;
import org.apache.ldap.server.interceptor.NextInterceptor;
import org.apache.ldap.server.partition.DirectoryPartitionNexus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* An {@link org.apache.ldap.server.interceptor.Interceptor} that manages and enforces schemas.
*
* @todo Better interceptor description required.
* @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
* @version $Rev$, $Date$
*/
public class SchemaService extends BaseInterceptor
{
private static final String[] EMPTY_STRING_ARRAY = new String[0];
private static final String BINARY_KEY = "java.naming.ldap.attributes.binary";
/** The LoggerFactory used by this Interceptor */
private static Logger log = LoggerFactory.getLogger( SchemaService.class );
/**
* the root nexus to all database partitions
*/
private DirectoryPartitionNexus nexus;
/**
* a binary attribute tranforming filter: String -> byte[]
*/
private BinaryAttributeFilter binaryAttributeFilter;
/**
* the global schema object registries
*/
private GlobalRegistries globalRegistries;
private Set binaries;
/**
* subschemaSubentry attribute's value from Root DSE
*/
private String subentryDn;
/**
* The time when the server started up.
*/
private String startUpTimeStamp;
/**
* Creates a schema service interceptor.
*/
public SchemaService()
{
startUpTimeStamp = DateUtils.getGeneralizedTime();
}
private void initBinaries( Hashtable env )
{
// construct the set for fast lookups while filtering
String binaryIds = (String)env.get( BINARY_KEY );
binaries = new HashSet();
if ( StringUtils.isEmpty( binaryIds ) == false )
{
String[] binaryArray = binaryIds.split( "/" );
for ( int i = 0; i < binaryArray.length; i++ )
{
binaries.add( StringUtils.lowerCase( StringUtils.trim( binaryArray[i] ) ) );
}
}
}
public void init( DirectoryServiceConfiguration factoryCfg, InterceptorConfiguration cfg ) throws NamingException
{
this.nexus = factoryCfg.getPartitionNexus();
this.globalRegistries = factoryCfg.getGlobalRegistries();
binaryAttributeFilter = new BinaryAttributeFilter();
initBinaries( factoryCfg.getEnvironment() );
// stuff for dealing with subentries (garbage for now)
String subschemaSubentry = ( String ) nexus.getRootDSE().get( "subschemaSubentry" ).get();
subentryDn = new LdapName( subschemaSubentry ).toString().toLowerCase();
}
/**
* @return Returns the binaries.
*/
public boolean isBinary( String id )
{
return binaries.contains( StringUtils.lowerCase( StringUtils.trim( id ) ) );
}
public void destroy()
{
}
public NamingEnumeration list( NextInterceptor nextInterceptor, Name base ) throws NamingException
{
NamingEnumeration e = nextInterceptor.list( base );
Invocation invocation = InvocationStack.getInstance().peek();
return new SearchResultFilteringEnumeration( e, new SearchControls(), invocation, binaryAttributeFilter );
}
public NamingEnumeration search( NextInterceptor nextInterceptor,
Name base, Map env, ExprNode filter,
SearchControls searchCtls ) throws NamingException
{
// check to make sure the DN searched for is a subentry
if ( !subentryDn.equals( base.toString() ) )
{
return nextInterceptor.search( base, env, filter, searchCtls );
}
if ( searchCtls.getSearchScope() == SearchControls.OBJECT_SCOPE &&
filter instanceof SimpleNode )
{
SimpleNode node = ( SimpleNode ) filter;
if ( node.getAttribute().equalsIgnoreCase( "objectClass" ) &&
node.getValue().equalsIgnoreCase( "subschema" ) &&
node.getAssertionType() == SimpleNode.EQUALITY
)
{
// call.setBypass( true );
Attributes attrs = getSubschemaEntry( searchCtls.getReturningAttributes() );
SearchResult result = new SearchResult( base.toString(), null, attrs );
return new SingletonEnumeration( result );
}
}
else if ( searchCtls.getSearchScope() == SearchControls.OBJECT_SCOPE &&
filter instanceof PresenceNode )
{
PresenceNode node = ( PresenceNode ) filter;
if ( node.getAttribute().equalsIgnoreCase( "objectClass" ) )
{
// call.setBypass( true );
Attributes attrs = getSubschemaEntry( searchCtls.getReturningAttributes() );
SearchResult result = new SearchResult( base.toString(), null, attrs );
return new SingletonEnumeration( result );
}
}
NamingEnumeration e = nextInterceptor.search( base, env, filter, searchCtls );
if ( searchCtls.getReturningAttributes() != null )
{
return e;
}
Invocation invocation = InvocationStack.getInstance().peek();
return new SearchResultFilteringEnumeration( e, searchCtls, invocation, binaryAttributeFilter );
}
private Attributes getSubschemaEntry( String[] ids ) throws NamingException
{
if ( ids == null )
{
ids = EMPTY_STRING_ARRAY;
}
Set set = new HashSet();
LockableAttributesImpl attrs = new LockableAttributesImpl();
LockableAttributeImpl attr;
for ( int ii = 0; ii < ids.length; ii++ )
{
set.add( ids[ii].toLowerCase() );
}
// Check whether the set contains a plus, and use it below to include all
// operational attributes. Due to RFC 3673, and issue DIREVE-228 in JIRA
boolean returnAllOperationalAttributes = set.contains( "+" );
if ( returnAllOperationalAttributes || set.contains( "objectclasses" ) )
{
attr = new LockableAttributeImpl( attrs, "objectClasses" );
Iterator list = globalRegistries.getObjectClassRegistry().list();
while ( list.hasNext() )
{
ObjectClass oc = ( ObjectClass ) list.next();
attr.add( SchemaUtils.render( oc ).toString() );
}
attrs.put( attr );
}
if ( returnAllOperationalAttributes || set.contains( "attributetypes" ) )
{
attr = new LockableAttributeImpl( attrs, "attributeTypes" );
Iterator list = globalRegistries.getAttributeTypeRegistry().list();
while ( list.hasNext() )
{
AttributeType at = ( AttributeType ) list.next();
attr.add( SchemaUtils.render( at ).toString() );
}
attrs.put( attr );
}
if ( returnAllOperationalAttributes || set.contains( "matchingrules" ) )
{
attr = new LockableAttributeImpl( attrs, "matchingRules" );
Iterator list = globalRegistries.getMatchingRuleRegistry().list();
while ( list.hasNext() )
{
MatchingRule mr = ( MatchingRule ) list.next();
attr.add( SchemaUtils.render( mr ).toString() );
}
attrs.put( attr );
}
if ( returnAllOperationalAttributes || set.contains( "matchingruleuse" ) )
{
attr = new LockableAttributeImpl( attrs, "matchingRuleUse" );
Iterator list = globalRegistries.getMatchingRuleUseRegistry().list();
while ( list.hasNext() )
{
MatchingRuleUse mru = ( MatchingRuleUse ) list.next();
attr.add( SchemaUtils.render( mru ).toString() );
}
attrs.put( attr );
}
if ( returnAllOperationalAttributes || set.contains( "ldapsyntaxes" ) )
{
attr = new LockableAttributeImpl( attrs, "ldapSyntaxes" );
Iterator list = globalRegistries.getSyntaxRegistry().list();
while ( list.hasNext() )
{
Syntax syntax = ( Syntax ) list.next();
attr.add( SchemaUtils.render( syntax ).toString() );
}
attrs.put( attr );
}
if ( returnAllOperationalAttributes || set.contains( "ditcontentrules" ) )
{
attr = new LockableAttributeImpl( attrs, "dITContentRules" );
Iterator list = globalRegistries.getDitContentRuleRegistry().list();
while ( list.hasNext() )
{
DITContentRule dcr = ( DITContentRule ) list.next();
attr.add( SchemaUtils.render( dcr ).toString() );
}
attrs.put( attr );
}
if ( returnAllOperationalAttributes || set.contains( "ditstructurerules" ) )
{
attr = new LockableAttributeImpl( attrs, "dITStructureRules" );
Iterator list = globalRegistries.getDitStructureRuleRegistry().list();
while ( list.hasNext() )
{
DITStructureRule dsr = ( DITStructureRule ) list.next();
attr.add( SchemaUtils.render( dsr ).toString() );
}
attrs.put( attr );
}
if ( returnAllOperationalAttributes || set.contains( "nameforms" ) )
{
attr = new LockableAttributeImpl( attrs, "nameForms" );
Iterator list = globalRegistries.getNameFormRegistry().list();
while ( list.hasNext() )
{
NameForm nf = ( NameForm ) list.next();
attr.add( SchemaUtils.render( nf ).toString() );
}
attrs.put( attr );
}
// timeestamps are hacks for now until the schema is actually updateable these
// use the servers startup time stamp for both modify and create timestamps
if ( returnAllOperationalAttributes || set.contains( "createtimestamp" ) )
{
attr = new LockableAttributeImpl( attrs, "createTimestamp" );
attr.add( startUpTimeStamp );
attrs.put( attr );
}
if ( returnAllOperationalAttributes || set.contains( "modifytimestamp" ) )
{
attr = new LockableAttributeImpl( attrs, "modifyTimestamp" );
attr.add( startUpTimeStamp );
attrs.put( attr );
}
if ( returnAllOperationalAttributes || set.contains( "creatorsname" ) )
{
attr = new LockableAttributeImpl( attrs, "creatorsName" );
attr.add( DirectoryPartitionNexus.ADMIN_PRINCIPAL );
attrs.put( attr );
}
if ( returnAllOperationalAttributes || set.contains( "modifiersname" ) )
{
attr = new LockableAttributeImpl( attrs, "modifiersName" );
attr.add( DirectoryPartitionNexus.ADMIN_PRINCIPAL );
attrs.put( attr );
}
int minSetSize = 0;
if ( set.contains( "+" ) )
{
minSetSize++;
}
if ( set.contains( "*" ) )
{
minSetSize++;
}
if ( set.contains( "ref" ) )
{
minSetSize++;
}
// add the objectClass attribute
if ( set.contains( "*" ) || set.contains( "objectclass" ) || set.size() == minSetSize )
{
attr = new LockableAttributeImpl( attrs, "objectClass" );
attr.add( "top" );
attr.add( "subschema" );
attrs.put( attr );
}
// add the cn attribute as required for the RDN
if ( set.contains( "*" ) || set.contains( "cn" ) || set.contains( "commonname" ) || set.size() == minSetSize )
{
attrs.put( "cn", "schema" );
}
return attrs;
}
public Attributes lookup( NextInterceptor nextInterceptor, Name name ) throws NamingException
{
Attributes result = nextInterceptor.lookup( name );
Invocation invocation = InvocationStack.getInstance().peek();
doFilter( invocation, result );
return result;
}
public Attributes lookup( NextInterceptor nextInterceptor, Name name, String[] attrIds ) throws NamingException
{
Attributes result = nextInterceptor.lookup( name, attrIds );
if ( result == null )
{
return null;
}
Invocation invocation = InvocationStack.getInstance().peek();
doFilter( invocation, result );
return result;
}
/**
* Checks to see if an attribute is required by as determined from an entry's
* set of objectClass attribute values.
*
* @param attrId the attribute to test if required by a set of objectClass values
* @param objectClass the objectClass values
* @return true if the objectClass values require the attribute, false otherwise
* @throws NamingException if the attribute is not recognized
*/
private boolean isRequired( String attrId, Attribute objectClass ) throws NamingException
{
OidRegistry oidRegistry = globalRegistries.getOidRegistry();
ObjectClassRegistry registry = globalRegistries.getObjectClassRegistry();
if ( ! oidRegistry.hasOid( attrId ) )
{
return false;
}
String attrOid = oidRegistry.getOid( attrId );
for ( int ii = 0; ii < objectClass.size(); ii++ )
{
ObjectClass ocSpec = registry.lookup( ( String ) objectClass.get( ii ) );
AttributeType[] mustList = ocSpec.getMustList();
for ( int jj = 0; jj < mustList.length; jj++ )
{
if ( mustList[jj].getOid().equals( attrOid ) )
{
return true;
}
}
}
return false;
}
/**
* Checks to see if removing a set of attributes from an entry completely removes
* that attribute's values. If change has zero size then all attributes are
* presumed to be removed.
*
* @param change
* @param entry
* @return
* @throws NamingException
*/
private boolean isCompleteRemoval( Attribute change, Attributes entry ) throws NamingException
{
// if change size is 0 then all values are deleted then we're screwed
if ( change.size() == 0 )
{
return true;
}
// can't do math to figure our if all values are removed since some
// values in the modify request may not be in the entry. we need to
// remove the values from a cloned version of the attribute and see
// if nothing is left.
Attribute changedEntryAttr = entry.get( change.getID() );
for ( int jj = 0; jj < change.size(); jj++ )
{
changedEntryAttr.remove( change.get( jj ) );
}
return changedEntryAttr.size() == 0;
}
Attribute getResultantObjectClasses( int modOp, Attribute changes, Attribute existing ) throws NamingException
{
if ( changes == null && existing == null )
{
return new LockableAttributeImpl( "objectClass" );
}
if ( changes == null )
{
return existing;
}
if ( existing == null && modOp == DirContext.ADD_ATTRIBUTE )
{
return changes;
}
else if ( existing == null )
{
return new LockableAttributeImpl( "objectClasses" );
}
switch( modOp )
{
case( DirContext.ADD_ATTRIBUTE ):
return AttributeUtils.getUnion( existing, changes );
case( DirContext.REPLACE_ATTRIBUTE ):
return ( Attribute ) changes.clone();
case( DirContext.REMOVE_ATTRIBUTE ):
return AttributeUtils.getDifference( existing, changes );
default:
throw new InternalError( "" );
}
}
public void modify( NextInterceptor next, Name name, int modOp, Attributes mods ) throws NamingException
{
Attributes entry = nexus.lookup( name );
Attribute objectClass = getResultantObjectClasses( modOp, mods.get( "objectClass"), entry.get( "objectClass" ) );
ObjectClassRegistry ocRegistry = this.globalRegistries.getObjectClassRegistry();
AttributeTypeRegistry atRegistry = this.globalRegistries.getAttributeTypeRegistry();
NamingEnumeration changes = mods.getIDs();
while ( changes.hasMore() )
{
String id = ( String ) changes.next();
Attribute change = mods.get( id );
if ( ! atRegistry.hasAttributeType( change.getID() ) && ! objectClass.contains( "extensibleObject" ) )
{
throw new LdapInvalidAttributeIdentifierException( "unrecognized attributeID " + change.getID() );
}
if ( modOp == DirContext.REMOVE_ATTRIBUTE && entry.get( change.getID() ) == null )
{
throw new LdapNoSuchAttributeException();
}
// for required attributes we need to check if all values are removed
// if so then we have a schema violation that must be thrown
if ( modOp == DirContext.REMOVE_ATTRIBUTE &&
isRequired( change.getID(), objectClass ) &&
isCompleteRemoval( change, entry ) )
{
throw new LdapSchemaViolationException( ResultCodeEnum.OBJECTCLASSVIOLATION );
}
}
if ( modOp == DirContext.REMOVE_ATTRIBUTE )
{
SchemaChecker.preventRdnChangeOnModifyRemove( name, modOp, mods );
SchemaChecker.preventStructuralClassRemovalOnModifyRemove( ocRegistry, name, modOp, mods, objectClass );
}
if ( modOp == DirContext.REPLACE_ATTRIBUTE )
{
SchemaChecker.preventRdnChangeOnModifyReplace( name, modOp, mods );
SchemaChecker.preventStructuralClassRemovalOnModifyReplace( ocRegistry, name, modOp, mods );
}
next.modify( name, modOp, mods );
}
public void modify( NextInterceptor next, Name name, ModificationItem[] mods ) throws NamingException
{
Attributes entry = nexus.lookup( name );
ModificationItem objectClassMod = null;
for ( int ii = 0; ii < mods.length; ii++ )
{
if ( mods[ii].getAttribute().getID().equalsIgnoreCase( "objectclass" ) )
{
objectClassMod = mods[ii];
}
}
Attribute objectClass;
if ( objectClassMod == null )
{
objectClass = entry.get( "objectClass" );
}
else
{
objectClass = getResultantObjectClasses( objectClassMod.getModificationOp(),
objectClassMod.getAttribute(), entry.get( "objectClass" ) );
}
ObjectClassRegistry ocRegistry = this.globalRegistries.getObjectClassRegistry();
AttributeTypeRegistry atRegistry = this.globalRegistries.getAttributeTypeRegistry();
for ( int ii = 0; ii < mods.length; ii++ )
{
int modOp = mods[ii].getModificationOp();
Attribute change = mods[ii].getAttribute();
if ( ! atRegistry.hasAttributeType( change.getID() ) && ! objectClass.contains( "extensibleObject" ) )
{
throw new LdapInvalidAttributeIdentifierException();
}
if ( modOp == DirContext.REMOVE_ATTRIBUTE && entry.get( change.getID() ) == null )
{
throw new LdapNoSuchAttributeException();
}
if ( modOp == DirContext.REMOVE_ATTRIBUTE )
{
// for required attributes we need to check if all values are removed
// if so then we have a schema violation that must be thrown
if ( isRequired( change.getID(), objectClass ) && isCompleteRemoval( change, entry ) )
{
throw new LdapSchemaViolationException( ResultCodeEnum.OBJECTCLASSVIOLATION );
}
SchemaChecker.preventRdnChangeOnModifyRemove( name, modOp, change );
SchemaChecker.preventStructuralClassRemovalOnModifyRemove( ocRegistry, name, modOp, change, objectClass );
}
if ( modOp == DirContext.REPLACE_ATTRIBUTE )
{
SchemaChecker.preventRdnChangeOnModifyReplace( name, modOp, change );
SchemaChecker.preventStructuralClassRemovalOnModifyReplace( ocRegistry, name, modOp, change );
}
}
next.modify( name, mods );
}
private void doFilter( Invocation invocation, Attributes entry )
throws NamingException
{
long t0 = System.nanoTime();
if ( log.isDebugEnabled() )
{
log.debug( "Filtering entry " + AttributeUtils.toString( entry ) );
}
/*
* start converting values of attributes to byte[]s which are not
* human readable and those that are in the binaries set
*/
NamingEnumeration list = entry.getIDs();
while ( list.hasMore() )
{
String id = ( String ) list.next();
AttributeType type = null;
boolean asBinary = false;
if ( globalRegistries.getAttributeTypeRegistry().hasAttributeType( id ) )
{
type = globalRegistries.getAttributeTypeRegistry().lookup( id );
}
if ( type != null )
{
asBinary = !type.getSyntax().isHumanReadible();
asBinary = asBinary || binaries.contains( type );
}
if ( asBinary )
{
Attribute attribute = entry.get( id );
Attribute binary = new LockableAttributeImpl( id );
for ( int i = 0; i < attribute.size(); i++ )
{
Object value = attribute.get( i );
if ( value instanceof String )
{
binary.add( i, StringUtils.getBytesUtf8( ( String ) value ) );
}
else
{
binary.add( i, value );
}
}
entry.remove( id );
entry.put( binary );
}
}
long t1 = System.nanoTime();
if ( log.isDebugEnabled() )
{
log.debug( "Time to filter entry = " + (t1 - t0) + "ns" );
}
}
/**
* A special filter over entry attributes which replaces Attribute String values with their respective byte[]
* representations using schema information and the value held in the JNDI environment property:
* <code>java.naming.ldap.attributes.binary</code>.
*
* @see <a href= "http://java.sun.com/j2se/1.4.2/docs/guide/jndi/jndi-ldap-gl.html#binary">
* java.naming.ldap.attributes.binary</a>
*/
private class BinaryAttributeFilter implements SearchResultFilter
{
public BinaryAttributeFilter()
{
}
public boolean accept( Invocation invocation, SearchResult result, SearchControls controls ) throws NamingException
{
doFilter( invocation, result.getAttributes() );
return true;
}
}
}