blob: 3cad96c2a11f272892df76b822af6a5c8c4f1d1f [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.directory.server.core.authz;
import org.apache.directory.server.constants.ServerDNConstants;
import org.apache.directory.server.core.DirectoryService;
import org.apache.directory.server.core.authn.LdapPrincipal;
import org.apache.directory.server.core.authz.support.ACDFEngine;
import org.apache.directory.server.core.enumeration.SearchResultFilter;
import org.apache.directory.server.core.enumeration.SearchResultFilteringEnumeration;
import org.apache.directory.server.core.interceptor.BaseInterceptor;
import org.apache.directory.server.core.interceptor.InterceptorChain;
import org.apache.directory.server.core.interceptor.NextInterceptor;
import org.apache.directory.server.core.interceptor.context.AddOperationContext;
import org.apache.directory.server.core.interceptor.context.CompareOperationContext;
import org.apache.directory.server.core.interceptor.context.DeleteOperationContext;
import org.apache.directory.server.core.interceptor.context.EntryOperationContext;
import org.apache.directory.server.core.interceptor.context.GetMatchedNameOperationContext;
import org.apache.directory.server.core.interceptor.context.ListOperationContext;
import org.apache.directory.server.core.interceptor.context.LookupOperationContext;
import org.apache.directory.server.core.interceptor.context.ModifyOperationContext;
import org.apache.directory.server.core.interceptor.context.MoveAndRenameOperationContext;
import org.apache.directory.server.core.interceptor.context.MoveOperationContext;
import org.apache.directory.server.core.interceptor.context.RenameOperationContext;
import org.apache.directory.server.core.interceptor.context.SearchOperationContext;
import org.apache.directory.server.core.invocation.Invocation;
import org.apache.directory.server.core.invocation.InvocationStack;
import org.apache.directory.server.core.jndi.ServerContext;
import org.apache.directory.server.core.jndi.ServerLdapContext;
import org.apache.directory.server.core.partition.PartitionNexusProxy;
import org.apache.directory.server.core.subtree.SubentryInterceptor;
import org.apache.directory.server.schema.ConcreteNameComponentNormalizer;
import org.apache.directory.server.schema.registries.AttributeTypeRegistry;
import org.apache.directory.server.schema.registries.OidRegistry;
import org.apache.directory.shared.ldap.aci.ACIItem;
import org.apache.directory.shared.ldap.aci.ACIItemParser;
import org.apache.directory.shared.ldap.aci.ACITuple;
import org.apache.directory.shared.ldap.aci.MicroOperation;
import org.apache.directory.shared.ldap.constants.SchemaConstants;
import org.apache.directory.shared.ldap.exception.LdapNamingException;
import org.apache.directory.shared.ldap.exception.LdapNoPermissionException;
import org.apache.directory.shared.ldap.message.ModificationItemImpl;
import org.apache.directory.shared.ldap.message.ResultCodeEnum;
import org.apache.directory.shared.ldap.name.LdapDN;
import org.apache.directory.shared.ldap.schema.AttributeType;
import org.apache.directory.shared.ldap.schema.SchemaUtils;
import org.apache.directory.shared.ldap.util.AttributeUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.naming.Name;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import java.text.ParseException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* An ACI based authorization service.
*
* @org.apache.xbean.XBean
*
* @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
* @version $Rev$
*/
public class AciAuthorizationInterceptor extends BaseInterceptor
{
/** the logger for this class */
private static final Logger LOG = LoggerFactory.getLogger( AciAuthorizationInterceptor.class );
/**
* the multivalued op attr used to track the perscriptive access control
* subentries that apply to an entry.
*/
private static final String AC_SUBENTRY_ATTR = "accessControlSubentries";
private static final Collection<MicroOperation> ADD_PERMS;
private static final Collection<MicroOperation> READ_PERMS;
private static final Collection<MicroOperation> COMPARE_PERMS;
private static final Collection<MicroOperation> SEARCH_ENTRY_PERMS;
private static final Collection<MicroOperation> SEARCH_ATTRVAL_PERMS;
private static final Collection<MicroOperation> REMOVE_PERMS;
private static final Collection<MicroOperation> MATCHEDNAME_PERMS;
private static final Collection<MicroOperation> BROWSE_PERMS;
private static final Collection<MicroOperation> LOOKUP_PERMS;
private static final Collection<MicroOperation> REPLACE_PERMS;
private static final Collection<MicroOperation> RENAME_PERMS;
private static final Collection<MicroOperation> EXPORT_PERMS;
private static final Collection<MicroOperation> IMPORT_PERMS;
private static final Collection<MicroOperation> MOVERENAME_PERMS;
static
{
Set<MicroOperation> set = new HashSet<MicroOperation>( 2 );
set.add( MicroOperation.BROWSE );
set.add( MicroOperation.RETURN_DN );
SEARCH_ENTRY_PERMS = Collections.unmodifiableCollection( set );
set = new HashSet<MicroOperation>( 2 );
set.add( MicroOperation.READ );
set.add( MicroOperation.BROWSE );
LOOKUP_PERMS = Collections.unmodifiableCollection( set );
set = new HashSet<MicroOperation>( 2 );
set.add( MicroOperation.ADD );
set.add( MicroOperation.REMOVE );
REPLACE_PERMS = Collections.unmodifiableCollection( set );
set = new HashSet<MicroOperation>( 2 );
set.add( MicroOperation.EXPORT );
set.add( MicroOperation.RENAME );
MOVERENAME_PERMS = Collections.unmodifiableCollection( set );
SEARCH_ATTRVAL_PERMS = Collections.singleton( MicroOperation.READ );
ADD_PERMS = Collections.singleton( MicroOperation.ADD );
READ_PERMS = Collections.singleton( MicroOperation.READ );
COMPARE_PERMS = Collections.singleton( MicroOperation.COMPARE );
REMOVE_PERMS = Collections.singleton( MicroOperation.REMOVE );
MATCHEDNAME_PERMS = Collections.singleton( MicroOperation.DISCLOSE_ON_ERROR );
BROWSE_PERMS = Collections.singleton( MicroOperation.BROWSE );
RENAME_PERMS = Collections.singleton( MicroOperation.RENAME );
EXPORT_PERMS = Collections.singleton( MicroOperation.EXPORT );
IMPORT_PERMS = Collections.singleton( MicroOperation.IMPORT );
}
/** a tupleCache that responds to add, delete, and modify attempts */
private TupleCache tupleCache;
/** a groupCache that responds to add, delete, and modify attempts */
private GroupCache groupCache;
/** a normalizing ACIItem parser */
private ACIItemParser aciParser;
/** use and instance of the ACDF engine */
private ACDFEngine engine;
/** interceptor chain */
private InterceptorChain chain;
/** attribute type registry */
private AttributeTypeRegistry attrRegistry;
/** whether or not this interceptor is activated */
private boolean enabled;
/** the system wide subschemaSubentryDn */
private String subschemaSubentryDn;
private AttributeType objectClassType;
private AttributeType acSubentryType;
private String subentryOid;
/** A storage for the entryACI attributeType */
private AttributeType entryAciType;
/** the subentry ACI attribute type */
private AttributeType subentryAciType;
public static final SearchControls DEFAULT_SEARCH_CONTROLS = new SearchControls();
/**
* Initializes this interceptor based service by getting a handle on the nexus, setting up
* the tupe and group membership caches and the ACIItem parser and the ACDF engine.
*
* @param directoryService the directory service core
* @throws NamingException if there are problems during initialization
*/
public void init( DirectoryService directoryService ) throws NamingException
{
super.init( directoryService );
tupleCache = new TupleCache( directoryService );
groupCache = new GroupCache( directoryService );
attrRegistry = directoryService.getRegistries().getAttributeTypeRegistry();
OidRegistry oidRegistry = directoryService.getRegistries().getOidRegistry();
// look up some constant information
String objectClassOid = oidRegistry.getOid( SchemaConstants.OBJECT_CLASS_AT );
subentryOid = oidRegistry.getOid( SchemaConstants.SUBENTRY_OC );
String acSubentryOid = oidRegistry.getOid( AC_SUBENTRY_ATTR );
objectClassType = attrRegistry.lookup( objectClassOid );
acSubentryType = attrRegistry.lookup( acSubentryOid );
entryAciType = attrRegistry.lookup( SchemaConstants.ENTRY_ACI_AT_OID );
subentryAciType = attrRegistry.lookup( SchemaConstants.SUBENTRY_ACI_AT_OID );
aciParser = new ACIItemParser( new ConcreteNameComponentNormalizer( attrRegistry, oidRegistry ), attrRegistry.getNormalizerMapping() );
engine = new ACDFEngine( directoryService.getRegistries().getOidRegistry(), attrRegistry );
chain = directoryService.getInterceptorChain();
enabled = directoryService.isAccessControlEnabled();
// stuff for dealing with subentries (garbage for now)
String subschemaSubentry =
( String ) directoryService.getPartitionNexus().getRootDSE( null ).
get( SchemaConstants.SUBSCHEMA_SUBENTRY_AT ).get();
LdapDN subschemaSubentryDnName = new LdapDN( subschemaSubentry );
subschemaSubentryDnName.normalize( attrRegistry.getNormalizerMapping() );
subschemaSubentryDn = subschemaSubentryDnName.toNormName();
}
private void protectCriticalEntries( LdapDN dn ) throws NamingException
{
LdapDN principalDn = getPrincipal().getJndiName();
if ( dn.isEmpty() )
{
String msg = "The rootDSE cannot be deleted, moved or renamed!";
LOG.error( msg );
throw new LdapNoPermissionException( msg );
}
if ( isTheAdministrator( dn ) )
{
String msg = "User '" + principalDn.getUpName();
msg += "' does not have permission to move or rename the admin";
msg += " account. No one not even the admin can del, move or";
msg += " rename " + dn.getUpName() + "!";
LOG.error( msg );
throw new LdapNoPermissionException( msg );
}
}
private LdapDN parseNormalized( String name ) throws NamingException
{
LdapDN dn = new LdapDN( name );
dn.normalize( attrRegistry.getNormalizerMapping() );
return dn;
}
/**
* Adds perscriptiveACI tuples to a collection of tuples by accessing the
* tupleCache. The tuple cache is accessed for each A/C subentry
* associated with the protected entry. Note that subentries are handled
* differently: their parent, the administrative entry is accessed to
* determine the perscriptiveACIs effecting the AP and hence the subentry
* which is considered to be in the same context.
*
* @param tuples the collection of tuples to add to
* @param dn the normalized distinguished name of the protected entry
* @param entry the target entry that access to is being controled
* @throws NamingException if there are problems accessing attribute values
* @param proxy the partition nexus proxy object
*/
private void addPerscriptiveAciTuples( PartitionNexusProxy proxy, Collection<ACITuple> tuples, LdapDN dn,
Attributes entry ) throws NamingException
{
Attribute oc = AttributeUtils.getAttribute( entry, objectClassType );
/*
* If the protected entry is a subentry, then the entry being evaluated
* for perscriptiveACIs is in fact the administrative entry. By
* substituting the administrative entry for the actual subentry the
* code below this "if" statement correctly evaluates the effects of
* perscriptiveACI on the subentry. Basically subentries are considered
* to be in the same naming context as their access point so the subentries
* effecting their parent entry applies to them as well.
*/
if ( AttributeUtils.containsValue( oc, SchemaConstants.SUBENTRY_OC, objectClassType ) ||
AttributeUtils.containsValue( oc, subentryOid, objectClassType ) )
{
LdapDN parentDn = ( LdapDN ) dn.clone();
parentDn.remove( dn.size() - 1 );
entry = proxy.lookup( new LookupOperationContext( parentDn), PartitionNexusProxy.LOOKUP_BYPASS );
}
Attribute subentries = AttributeUtils.getAttribute( entry, acSubentryType );
if ( subentries == null )
{
return;
}
for ( int ii = 0; ii < subentries.size(); ii++ )
{
String subentryDn = ( String ) subentries.get( ii );
tuples.addAll( tupleCache.getACITuples( subentryDn ) );
}
}
/**
* Adds the set of entryACI tuples to a collection of tuples. The entryACI
* is parsed and tuples are generated on they fly then added to the collection.
*
* @param tuples the collection of tuples to add to
* @param entry the target entry that access to is being regulated
* @throws NamingException if there are problems accessing attribute values
*/
private void addEntryAciTuples( Collection<ACITuple> tuples, Attributes entry ) throws NamingException
{
Attribute entryAci = AttributeUtils.getAttribute( entry, entryAciType );
if ( entryAci == null )
{
return;
}
for ( int ii = 0; ii < entryAci.size(); ii++ )
{
String aciString = ( String ) entryAci.get( ii );
ACIItem item;
try
{
item = aciParser.parse( aciString );
}
catch ( ParseException e )
{
String msg = "failed to parse entryACI: " + aciString;
LOG.error( msg, e );
throw new LdapNamingException( msg, ResultCodeEnum.OPERATIONS_ERROR );
}
tuples.addAll( item.toTuples() );
}
}
/**
* Adds the set of subentryACI tuples to a collection of tuples. The subentryACI
* is parsed and tuples are generated on the fly then added to the collection.
*
* @param tuples the collection of tuples to add to
* @param dn the normalized distinguished name of the protected entry
* @param entry the target entry that access to is being regulated
* @throws NamingException if there are problems accessing attribute values
* @param proxy the partition nexus proxy object
*/
private void addSubentryAciTuples( PartitionNexusProxy proxy, Collection<ACITuple> tuples, LdapDN dn, Attributes entry )
throws NamingException
{
// only perform this for subentries
if ( !AttributeUtils.containsValueCaseIgnore( entry.get( SchemaConstants.OBJECT_CLASS_AT ), SchemaConstants.SUBENTRY_OC ) )
{
return;
}
// get the parent or administrative entry for this subentry since it
// will contain the subentryACI attributes that effect subentries
LdapDN parentDn = ( LdapDN ) dn.clone();
parentDn.remove( dn.size() - 1 );
Attributes administrativeEntry = proxy.lookup(
new LookupOperationContext( parentDn, new String[]
{ SchemaConstants.SUBENTRY_ACI_AT }) , PartitionNexusProxy.LOOKUP_BYPASS );
Attribute subentryAci = AttributeUtils.getAttribute( administrativeEntry, subentryAciType );
if ( subentryAci == null )
{
return;
}
for ( int ii = 0; ii < subentryAci.size(); ii++ )
{
String aciString = ( String ) subentryAci.get( ii );
ACIItem item;
try
{
item = aciParser.parse( aciString );
}
catch ( ParseException e )
{
String msg = "failed to parse subentryACI: " + aciString;
LOG.error( msg, e );
throw new LdapNamingException( msg, ResultCodeEnum.OPERATIONS_ERROR );
}
tuples.addAll( item.toTuples() );
}
}
/* -------------------------------------------------------------------------------
* Within every access controled interceptor method we must retrieve the ACITuple
* set for all the perscriptiveACIs that apply to the candidate, the target entry
* operated upon. This ACITuple set is gotten from the TupleCache by looking up
* the subentries referenced by the accessControlSubentries operational attribute
* within the target entry.
*
* Then the entry is inspected for an entryACI. This is not done for the add op
* since it could introduce a security breech. So for non-add ops if present a
* set of ACITuples are generated for all the entryACIs within the entry. This
* set is combined with the ACITuples cached for the perscriptiveACI affecting
* the target entry. If the entry is a subentry the ACIs are also processed for
* the subentry to generate more ACITuples. This subentry TupleACI set is joined
* with the entry and perscriptive ACI.
*
* The union of ACITuples are fed into the engine along with other parameters
* to decide whether a permission is granted or rejected for the specific
* operation.
* -------------------------------------------------------------------------------
*/
public void add( NextInterceptor next, AddOperationContext addContext ) throws NamingException
{
// Access the principal requesting the operation, and bypass checks if it is the admin
Invocation invocation = InvocationStack.getInstance().peek();
LdapPrincipal principal = ( ( ServerContext ) invocation.getCaller() ).getPrincipal();
LdapDN principalDn = principal.getJndiName();
Attributes entry = addContext.getEntry();
LdapDN name = addContext.getDn();
// bypass authz code if we are disabled
if ( !enabled )
{
next.add( addContext );
return;
}
// bypass authz code but manage caches if operation is performed by the admin
if ( isPrincipalAnAdministrator( principalDn ) )
{
next.add( addContext );
tupleCache.subentryAdded( name, entry );
groupCache.groupAdded( name, entry );
return;
}
// perform checks below here for all non-admin users
SubentryInterceptor subentryInterceptor = ( SubentryInterceptor ) chain.get( SubentryInterceptor.class.getName() );
Attributes subentryAttrs = subentryInterceptor.getSubentryAttributes( name, entry );
NamingEnumeration<? extends Attribute> attrList = entry.getAll();
while ( attrList.hasMore() )
{
subentryAttrs.put( attrList.next() );
}
// Assemble all the information required to make an access control decision
Set<Name> userGroups = groupCache.getGroups( principalDn.toNormName() );
Collection<ACITuple> tuples = new HashSet<ACITuple>();
// Build the total collection of tuples to be considered for add rights
// NOTE: entryACI are NOT considered in adds (it would be a security breech)
addPerscriptiveAciTuples( invocation.getProxy(), tuples, name, subentryAttrs );
addSubentryAciTuples( invocation.getProxy(), tuples, name, subentryAttrs );
// check if entry scope permission is granted
PartitionNexusProxy proxy = invocation.getProxy();
engine.checkPermission( proxy, userGroups, principalDn, principal.getAuthenticationLevel(), name, null, null,
ADD_PERMS, tuples, subentryAttrs, null );
// now we must check if attribute type and value scope permission is granted
NamingEnumeration<? extends Attribute> attributeList = entry.getAll();
while ( attributeList.hasMore() )
{
Attribute attr = attributeList.next();
for ( int ii = 0; ii < attr.size(); ii++ )
{
engine.checkPermission( proxy, userGroups, principalDn, principal.getAuthenticationLevel(), name, attr
.getID(), attr.get( ii ), ADD_PERMS, tuples, entry, null );
}
}
// if we've gotten this far then access has been granted
next.add( addContext );
// if the entry added is a subentry or a groupOf[Unique]Names we must
// update the ACITuple cache and the groups cache to keep them in sync
tupleCache.subentryAdded( name, entry );
groupCache.groupAdded( name, entry );
}
private boolean isTheAdministrator( LdapDN normalizedDn )
{
return normalizedDn.getNormName().equals( ServerDNConstants.ADMIN_SYSTEM_DN_NORMALIZED );
}
public void delete( NextInterceptor next, DeleteOperationContext deleteContext ) throws NamingException
{
LdapDN name = deleteContext.getDn();
// Access the principal requesting the operation, and bypass checks if it is the admin
Invocation invocation = InvocationStack.getInstance().peek();
PartitionNexusProxy proxy = invocation.getProxy();
Attributes entry = proxy.lookup( new LookupOperationContext( name ) , PartitionNexusProxy.LOOKUP_BYPASS );
LdapPrincipal principal = ( ( ServerContext ) invocation.getCaller() ).getPrincipal();
LdapDN principalDn = principal.getJndiName();
// bypass authz code if we are disabled
if ( ! enabled )
{
next.delete( deleteContext );
return;
}
protectCriticalEntries( name );
// bypass authz code but manage caches if operation is performed by the admin
if ( isPrincipalAnAdministrator( principalDn ) )
{
next.delete( deleteContext );
tupleCache.subentryDeleted( name, entry );
groupCache.groupDeleted( name, entry );
return;
}
Set<Name> userGroups = groupCache.getGroups( principalDn.toString() );
Collection<ACITuple> tuples = new HashSet<ACITuple>();
addPerscriptiveAciTuples( proxy, tuples, name, entry );
addEntryAciTuples( tuples, entry );
addSubentryAciTuples( proxy, tuples, name, entry );
engine.checkPermission( proxy, userGroups, principalDn, principal.getAuthenticationLevel(), name, null, null,
REMOVE_PERMS, tuples, entry, null );
next.delete( deleteContext );
tupleCache.subentryDeleted( name, entry );
groupCache.groupDeleted( name, entry );
}
public void modify( NextInterceptor next, ModifyOperationContext opContext ) throws NamingException
{
// Access the principal requesting the operation, and bypass checks if it is the admin
Invocation invocation = InvocationStack.getInstance().peek();
PartitionNexusProxy proxy = invocation.getProxy();
LdapDN name = opContext.getDn();
// Access the principal requesting the operation, and bypass checks if it is the admin
Attributes entry = proxy.lookup( new LookupOperationContext( name ), PartitionNexusProxy.LOOKUP_BYPASS );
LdapPrincipal principal = ( ( ServerContext ) invocation.getCaller() ).getPrincipal();
LdapDN principalDn = principal.getJndiName();
// bypass authz code if we are disabled
if ( !enabled )
{
next.modify( opContext );
return;
}
List<ModificationItemImpl> mods = opContext.getModItems();
// bypass authz code but manage caches if operation is performed by the admin
if ( isPrincipalAnAdministrator( principalDn ) )
{
next.modify( opContext );
/**
* @TODO: A virtual entry can be created here for not hitting the backend again.
*/
Attributes modifiedEntry = proxy.lookup( new LookupOperationContext( name ), PartitionNexusProxy.LOOKUP_BYPASS );
tupleCache.subentryModified( name, mods, modifiedEntry );
groupCache.groupModified( name, mods, entry );
return;
}
Set<Name> userGroups = groupCache.getGroups( principalDn.toString() );
Collection<ACITuple> tuples = new HashSet<ACITuple>();
addPerscriptiveAciTuples( proxy, tuples, name, entry );
addEntryAciTuples( tuples, entry );
addSubentryAciTuples( proxy, tuples, name, entry );
engine.checkPermission( proxy, userGroups, principalDn, principal.getAuthenticationLevel(), name, null, null,
Collections.singleton( MicroOperation.MODIFY ), tuples, entry, null );
Collection<MicroOperation> perms = null;
Attributes entryView = ( Attributes ) entry.clone();
for ( ModificationItemImpl mod : mods )
{
Attribute attr = mod.getAttribute();
switch ( mod.getModificationOp() )
{
case ( DirContext.ADD_ATTRIBUTE ):
perms = ADD_PERMS;
// If the attribute is being created with an initial value ...
if ( entry.get( attr.getID() ) == null )
{
// ... we also need to check if adding the attribute is permitted
engine.checkPermission( proxy, userGroups, principalDn, principal.getAuthenticationLevel(), name,
attr.getID(), null, perms, tuples, entry, null );
}
break;
case ( DirContext.REMOVE_ATTRIBUTE ):
perms = REMOVE_PERMS;
Attribute entryAttr = entry.get( attr.getID() );
if ( entryAttr != null )
{
// If there is only one value remaining in the attribute ...
if ( entryAttr.size() == 1 )
{
// ... we also need to check if removing the attribute at all is permitted
engine.checkPermission( proxy, userGroups, principalDn, principal.getAuthenticationLevel(), name,
attr.getID(), null, perms, tuples, entry, null );
}
}
break;
case ( DirContext.REPLACE_ATTRIBUTE ):
perms = REPLACE_PERMS;
break;
}
/**
* Update the entry view as the current modification is applied to the original entry.
* This is especially required for handling the MaxValueCount protected item. Number of
* values for an attribute after a modification should be known in advance in order to
* check permissions for MaxValueCount protected item. So during addition of the first
* value of an attribute it can be rejected if the permission denied due the the
* MaxValueCount protected item. This is not the perfect implementation as required by
* the specification because the system should reject the addition exactly on the right
* value of the attribute. However as we do not have that much granularity in our
* implementation (we consider an Attribute Addition itself a Micro Operation,
* not the individual Value Additions) we just handle this when the first value of an
* attribute is being checked for relevant permissions below.
*/
entryView = SchemaUtils.getTargetEntry( mod, entryView );
for ( int jj = 0; jj < attr.size(); jj++ )
{
engine.checkPermission( proxy, userGroups, principalDn, principal.getAuthenticationLevel(), name,
attr.getID(), attr.get( jj ), perms, tuples, entry, entryView );
}
}
next.modify( opContext );
/**
* @TODO: A virtual entry can be created here for not hitting the backend again.
*/
Attributes modifiedEntry = proxy.lookup( new LookupOperationContext( name ), PartitionNexusProxy.LOOKUP_BYPASS );
tupleCache.subentryModified( name, mods, modifiedEntry );
groupCache.groupModified( name, mods, entry );
}
public boolean hasEntry( NextInterceptor next, EntryOperationContext entryContext ) throws NamingException
{
LdapDN name = entryContext.getDn();
Invocation invocation = InvocationStack.getInstance().peek();
PartitionNexusProxy proxy = invocation.getProxy();
Attributes entry = proxy.lookup( new LookupOperationContext( name ), PartitionNexusProxy.LOOKUP_BYPASS );
LdapPrincipal principal = ( ( ServerContext ) invocation.getCaller() ).getPrincipal();
LdapDN principalDn = principal.getJndiName();
if ( isPrincipalAnAdministrator( principalDn ) || !enabled || ( name.size() == 0 ) ) // no checks on the rootdse
{
// No need to go down to the stack, if the dn is empty : it's the rootDSE, and it exists !
return name.size() == 0 || next.hasEntry( entryContext );
}
Set<Name> userGroups = groupCache.getGroups( principalDn.toNormName() );
Collection<ACITuple> tuples = new HashSet<ACITuple>();
addPerscriptiveAciTuples( proxy, tuples, name, entry );
addEntryAciTuples( tuples, entry );
addSubentryAciTuples( proxy, tuples, name, entry );
// check that we have browse access to the entry
engine.checkPermission( proxy, userGroups, principalDn, principal.getAuthenticationLevel(), name, null, null,
BROWSE_PERMS, tuples, entry, null );
return next.hasEntry( entryContext );
}
/**
* Checks if the READ permissions exist to the entry and to each attribute type and
* value.
*
* @todo not sure if we should hide attribute types/values or throw an exception
* instead. I think we're going to have to use a filter to restrict the return
* of attribute types and values instead of throwing an exception. Lack of read
* perms to attributes and their values results in their removal when returning
* the entry.
*
* @param principal the user associated with the call
* @param dn the name of the entry being looked up
* @param entry the raw entry pulled from the nexus
* @throws NamingException if undlying access to the DIT fails
*/
private void checkLookupAccess( LdapPrincipal principal, LdapDN dn, Attributes entry ) throws NamingException
{
// no permissions checks on the RootDSE
if ( dn.toString().trim().equals( "" ) )
{
return;
}
PartitionNexusProxy proxy = InvocationStack.getInstance().peek().getProxy();
LdapDN userName = principal.getJndiName();
Set<Name> userGroups = groupCache.getGroups( userName.toNormName() );
Collection<ACITuple> tuples = new HashSet<ACITuple>();
addPerscriptiveAciTuples( proxy, tuples, dn, entry );
addEntryAciTuples( tuples, entry );
addSubentryAciTuples( proxy, tuples, dn, entry );
// check that we have read access to the entry
engine.checkPermission( proxy, userGroups, userName, principal.getAuthenticationLevel(), dn, null, null,
LOOKUP_PERMS, tuples, entry, null );
// check that we have read access to every attribute type and value
NamingEnumeration<? extends Attribute> attributeList = entry.getAll();
while ( attributeList.hasMore() )
{
Attribute attr = attributeList.next();
for ( int ii = 0; ii < attr.size(); ii++ )
{
engine.checkPermission( proxy, userGroups, userName, principal.getAuthenticationLevel(), dn, attr
.getID(), attr.get( ii ), READ_PERMS, tuples, entry, null );
}
}
}
public Attributes lookup( NextInterceptor next, LookupOperationContext lookupContext ) throws NamingException
{
Invocation invocation = InvocationStack.getInstance().peek();
LdapPrincipal principal = ( ( ServerContext ) invocation.getCaller() ).getPrincipal();
LdapDN principalDn = principal.getJndiName();
if ( !principalDn.isNormalized() )
{
principalDn.normalize( attrRegistry.getNormalizerMapping() );
}
if ( isPrincipalAnAdministrator( principalDn ) || !enabled )
{
return next.lookup( lookupContext );
}
PartitionNexusProxy proxy = invocation.getProxy();
Attributes entry = proxy.lookup( lookupContext, PartitionNexusProxy.LOOKUP_BYPASS );
checkLookupAccess( principal, lookupContext.getDn(), entry );
return next.lookup( lookupContext );
}
public void rename( NextInterceptor next, RenameOperationContext renameContext ) throws NamingException
{
LdapDN name = renameContext.getDn();
// Access the principal requesting the operation, and bypass checks if it is the admin
Invocation invocation = InvocationStack.getInstance().peek();
PartitionNexusProxy proxy = invocation.getProxy();
Attributes entry = proxy.lookup( new LookupOperationContext( name ), PartitionNexusProxy.LOOKUP_BYPASS );
LdapPrincipal principal = ( ( ServerContext ) invocation.getCaller() ).getPrincipal();
LdapDN principalDn = principal.getJndiName();
LdapDN newName = ( LdapDN ) name.clone();
newName.remove( name.size() - 1 );
newName.add( ( String ) renameContext.getNewRdn().getValue() );
// bypass authz code if we are disabled
if ( !enabled )
{
next.rename( renameContext );
return;
}
protectCriticalEntries( name );
// bypass authz code but manage caches if operation is performed by the admin
if ( isPrincipalAnAdministrator( principalDn ) )
{
next.rename( renameContext );
tupleCache.subentryRenamed( name, newName );
// TODO : this method returns a boolean : what should we do with the result ?
groupCache.groupRenamed( name, newName );
return;
}
Set<Name> userGroups = groupCache.getGroups( principalDn.toString() );
Collection<ACITuple> tuples = new HashSet<ACITuple>();
addPerscriptiveAciTuples( proxy, tuples, name, entry );
addEntryAciTuples( tuples, entry );
addSubentryAciTuples( proxy, tuples, name, entry );
engine.checkPermission( proxy, userGroups, principalDn, principal.getAuthenticationLevel(), name, null, null,
RENAME_PERMS, tuples, entry, null );
next.rename( renameContext );
tupleCache.subentryRenamed( name, newName );
groupCache.groupRenamed( name, newName );
}
public void moveAndRename( NextInterceptor next, MoveAndRenameOperationContext moveAndRenameContext )
throws NamingException
{
LdapDN oriChildName = moveAndRenameContext.getDn();
LdapDN newParentName = moveAndRenameContext.getParent();
// Access the principal requesting the operation, and bypass checks if it is the admin
Invocation invocation = InvocationStack.getInstance().peek();
PartitionNexusProxy proxy = invocation.getProxy();
Attributes entry = proxy.lookup( new LookupOperationContext( oriChildName ), PartitionNexusProxy.LOOKUP_BYPASS );
LdapPrincipal principal = ( ( ServerContext ) invocation.getCaller() ).getPrincipal();
LdapDN principalDn = principal.getJndiName();
LdapDN newName = ( LdapDN ) newParentName.clone();
newName.add( moveAndRenameContext.getNewRdn().getUpName() );
// bypass authz code if we are disabled
if ( !enabled )
{
next.moveAndRename( moveAndRenameContext );
return;
}
protectCriticalEntries( oriChildName );
// bypass authz code but manage caches if operation is performed by the admin
if ( isPrincipalAnAdministrator( principalDn ) )
{
next.moveAndRename( moveAndRenameContext );
tupleCache.subentryRenamed( oriChildName, newName );
groupCache.groupRenamed( oriChildName, newName );
return;
}
Set<Name> userGroups = groupCache.getGroups( principalDn.toString() );
Collection<ACITuple> tuples = new HashSet<ACITuple>();
addPerscriptiveAciTuples( proxy, tuples, oriChildName, entry );
addEntryAciTuples( tuples, entry );
addSubentryAciTuples( proxy, tuples, oriChildName, entry );
engine.checkPermission( proxy, userGroups, principalDn, principal.getAuthenticationLevel(), oriChildName, null,
null, MOVERENAME_PERMS, tuples, entry, null );
// Get the entry again without operational attributes
// because access control subentry operational attributes
// will not be valid at the new location.
// This will certainly be fixed by the SubentryInterceptor,
// but after this service.
Attributes importedEntry = proxy.lookup( new LookupOperationContext( oriChildName ),
PartitionNexusProxy.LOOKUP_EXCLUDING_OPR_ATTRS_BYPASS );
// As the target entry does not exist yet and so
// its subentry operational attributes are not there,
// we need to construct an entry to represent it
// at least with minimal requirements which are object class
// and access control subentry operational attributes.
SubentryInterceptor subentryInterceptor = ( SubentryInterceptor ) chain.get( SubentryInterceptor.class.getName() );
Attributes subentryAttrs = subentryInterceptor.getSubentryAttributes( newName, importedEntry );
NamingEnumeration<? extends Attribute> attrList = importedEntry.getAll();
while ( attrList.hasMore() )
{
subentryAttrs.put( attrList.next() );
}
Collection<ACITuple> destTuples = new HashSet<ACITuple>();
// Import permission is only valid for prescriptive ACIs
addPerscriptiveAciTuples( proxy, destTuples, newName, subentryAttrs );
// Evaluate the target context to see whether it
// allows an entry named newName to be imported as a subordinate.
engine.checkPermission( proxy, userGroups, principalDn, principal.getAuthenticationLevel(), newName, null,
null, IMPORT_PERMS, destTuples, subentryAttrs, null );
next.moveAndRename( moveAndRenameContext );
tupleCache.subentryRenamed( oriChildName, newName );
groupCache.groupRenamed( oriChildName, newName );
}
public void move( NextInterceptor next, MoveOperationContext moveContext ) throws NamingException
{
LdapDN oriChildName = moveContext.getDn();
LdapDN newParentName = moveContext.getParent();
// Access the principal requesting the operation, and bypass checks if it is the admin
Invocation invocation = InvocationStack.getInstance().peek();
PartitionNexusProxy proxy = invocation.getProxy();
Attributes entry = proxy.lookup( new LookupOperationContext( oriChildName ), PartitionNexusProxy.LOOKUP_BYPASS );
LdapDN newName = ( LdapDN ) newParentName.clone();
newName.add( oriChildName.get( oriChildName.size() - 1 ) );
LdapPrincipal principal = ( ( ServerContext ) invocation.getCaller() ).getPrincipal();
LdapDN principalDn = principal.getJndiName();
// bypass authz code if we are disabled
if ( !enabled )
{
next.move( moveContext );
return;
}
protectCriticalEntries( oriChildName);
// bypass authz code but manage caches if operation is performed by the admin
if ( isPrincipalAnAdministrator( principalDn ) )
{
next.move( moveContext );
tupleCache.subentryRenamed( oriChildName, newName );
groupCache.groupRenamed( oriChildName, newName );
return;
}
Set<Name> userGroups = groupCache.getGroups( principalDn.toString() );
Collection<ACITuple> tuples = new HashSet<ACITuple>();
addPerscriptiveAciTuples( proxy, tuples, oriChildName, entry );
addEntryAciTuples( tuples, entry );
addSubentryAciTuples( proxy, tuples, oriChildName, entry );
engine.checkPermission( proxy, userGroups, principalDn, principal.getAuthenticationLevel(), oriChildName, null,
null, EXPORT_PERMS, tuples, entry, null );
// Get the entry again without operational attributes
// because access control subentry operational attributes
// will not be valid at the new location.
// This will certainly be fixed by the SubentryInterceptor,
// but after this service.
Attributes importedEntry = proxy.lookup( new LookupOperationContext( oriChildName ),
PartitionNexusProxy.LOOKUP_EXCLUDING_OPR_ATTRS_BYPASS );
// As the target entry does not exist yet and so
// its subentry operational attributes are not there,
// we need to construct an entry to represent it
// at least with minimal requirements which are object class
// and access control subentry operational attributes.
SubentryInterceptor subentryInterceptor = ( SubentryInterceptor ) chain.get( SubentryInterceptor.class.getName() );
Attributes subentryAttrs = subentryInterceptor.getSubentryAttributes( newName, importedEntry );
NamingEnumeration<? extends Attribute> attrList = importedEntry.getAll();
while ( attrList.hasMore() )
{
subentryAttrs.put( attrList.next() );
}
Collection<ACITuple> destTuples = new HashSet<ACITuple>();
// Import permission is only valid for prescriptive ACIs
addPerscriptiveAciTuples( proxy, destTuples, newName, subentryAttrs );
// Evaluate the target context to see whether it
// allows an entry named newName to be imported as a subordinate.
engine.checkPermission( proxy, userGroups, principalDn, principal.getAuthenticationLevel(), newName, null,
null, IMPORT_PERMS, destTuples, subentryAttrs, null );
next.move( moveContext );
tupleCache.subentryRenamed( oriChildName, newName );
groupCache.groupRenamed( oriChildName, newName );
}
public NamingEnumeration<SearchResult> list( NextInterceptor next, ListOperationContext opContext ) throws NamingException
{
Invocation invocation = InvocationStack.getInstance().peek();
ServerLdapContext ctx = ( ServerLdapContext ) invocation.getCaller();
LdapPrincipal user = ctx.getPrincipal();
NamingEnumeration<SearchResult> e = next.list( opContext );
if ( isPrincipalAnAdministrator( user.getJndiName() ) || !enabled )
{
return e;
}
AuthorizationFilter authzFilter = new AuthorizationFilter();
return new SearchResultFilteringEnumeration( e, DEFAULT_SEARCH_CONTROLS, invocation, authzFilter, "List authorization Filter" );
}
public NamingEnumeration<SearchResult> search( NextInterceptor next, SearchOperationContext opContext ) throws NamingException
{
Invocation invocation = InvocationStack.getInstance().peek();
ServerLdapContext ctx = ( ServerLdapContext ) invocation.getCaller();
LdapPrincipal user = ctx.getPrincipal();
LdapDN principalDn = user.getJndiName();
NamingEnumeration<SearchResult> e = next.search( opContext );
boolean isSubschemaSubentryLookup = subschemaSubentryDn.equals( opContext.getDn().getNormName() );
SearchControls searchCtls = opContext.getSearchControls();
boolean isRootDSELookup = opContext.getDn().size() == 0 && searchCtls.getSearchScope() == SearchControls.OBJECT_SCOPE;
if ( isPrincipalAnAdministrator( principalDn ) || !enabled || isRootDSELookup || isSubschemaSubentryLookup )
{
return e;
}
AuthorizationFilter authzFilter = new AuthorizationFilter();
return new SearchResultFilteringEnumeration( e, searchCtls, invocation, authzFilter, "Search authorization Filter" );
}
public final boolean isPrincipalAnAdministrator( LdapDN principalDn )
{
return groupCache.isPrincipalAnAdministrator( principalDn );
}
public boolean compare( NextInterceptor next, CompareOperationContext opContext ) throws NamingException
{
LdapDN name = opContext.getDn();
String oid = opContext.getOid();
Object value = opContext.getValue();
// Access the principal requesting the operation, and bypass checks if it is the admin
Invocation invocation = InvocationStack.getInstance().peek();
PartitionNexusProxy proxy = invocation.getProxy();
Attributes entry = proxy.lookup(
new LookupOperationContext( name ),
PartitionNexusProxy.LOOKUP_BYPASS );
LdapPrincipal principal = ( ( ServerContext ) invocation.getCaller() ).getPrincipal();
LdapDN principalDn = principal.getJndiName();
if ( isPrincipalAnAdministrator( principalDn ) || !enabled )
{
return next.compare( opContext );
}
Set<Name> userGroups = groupCache.getGroups( principalDn.toNormName() );
Collection<ACITuple> tuples = new HashSet<ACITuple>();
addPerscriptiveAciTuples( proxy, tuples, name, entry );
addEntryAciTuples( tuples, entry );
addSubentryAciTuples( proxy, tuples, name, entry );
engine.checkPermission( proxy, userGroups, principalDn, principal.getAuthenticationLevel(), name, null, null,
READ_PERMS, tuples, entry, null );
engine.checkPermission( proxy, userGroups, principalDn, principal.getAuthenticationLevel(), name, oid, value,
COMPARE_PERMS, tuples, entry, null );
return next.compare( opContext );
}
public LdapDN getMatchedName ( NextInterceptor next, GetMatchedNameOperationContext opContext ) throws NamingException
{
// Access the principal requesting the operation, and bypass checks if it is the admin
Invocation invocation = InvocationStack.getInstance().peek();
PartitionNexusProxy proxy = invocation.getProxy();
LdapPrincipal principal = ( ( ServerContext ) invocation.getCaller() ).getPrincipal();
LdapDN principalDn = principal.getJndiName();
if ( isPrincipalAnAdministrator( principalDn ) || !enabled )
{
return next.getMatchedName( opContext );
}
// get the present matched name
Attributes entry;
LdapDN matched = next.getMatchedName( opContext );
// check if we have disclose on error permission for the entry at the matched dn
// if not remove rdn and check that until nothing is left in the name and return
// that but if permission is granted then short the process and return the dn
while ( matched.size() > 0 )
{
entry = proxy.lookup( new LookupOperationContext( matched ), PartitionNexusProxy.GETMATCHEDDN_BYPASS );
Set<Name> userGroups = groupCache.getGroups( principalDn.toString() );
Collection<ACITuple> tuples = new HashSet<ACITuple>();
addPerscriptiveAciTuples( proxy, tuples, matched, entry );
addEntryAciTuples( tuples, entry );
addSubentryAciTuples( proxy, tuples, matched, entry );
if ( engine.hasPermission( proxy, userGroups, principalDn, principal.getAuthenticationLevel(), matched, null,
null, MATCHEDNAME_PERMS, tuples, entry, null ) )
{
return matched;
}
matched.remove( matched.size() - 1 );
}
return matched;
}
public void cacheNewGroup( LdapDN name, Attributes entry ) throws NamingException
{
groupCache.groupAdded( name, entry );
}
private boolean filter( Invocation invocation, LdapDN normName, SearchResult result ) throws NamingException
{
/*
* First call hasPermission() for entry level "Browse" and "ReturnDN" perm
* tests. If we hasPermission() returns false we immediately short the
* process and return false.
*/
Attributes entry = invocation.getProxy().lookup( new LookupOperationContext( normName ), PartitionNexusProxy.LOOKUP_BYPASS );
ServerLdapContext ctx = ( ServerLdapContext ) invocation.getCaller();
LdapDN userDn = ctx.getPrincipal().getJndiName();
Set<Name> userGroups = groupCache.getGroups( userDn.toNormName() );
Collection<ACITuple> tuples = new HashSet<ACITuple>();
addPerscriptiveAciTuples( invocation.getProxy(), tuples, normName, entry );
addEntryAciTuples( tuples, entry );
addSubentryAciTuples( invocation.getProxy(), tuples, normName, entry );
if ( !engine.hasPermission( invocation.getProxy(), userGroups, userDn, ctx.getPrincipal()
.getAuthenticationLevel(), normName, null, null, SEARCH_ENTRY_PERMS, tuples, entry, null ) )
{
return false;
}
/*
* For each attribute type we check if access is allowed to the type. If not
* the attribute is yanked out of the entry to be returned. If permission is
* allowed we move on to check if the values are allowed. Values that are
* not allowed are removed from the attribute. If the attribute has no more
* values remaining then the entire attribute is removed.
*/
NamingEnumeration<String> idList = result.getAttributes().getIDs();
while ( idList.hasMore() )
{
// if attribute type scope access is not allowed then remove the attribute and continue
String id = idList.next();
Attribute attr = result.getAttributes().get( id );
if ( !engine.hasPermission( invocation.getProxy(), userGroups, userDn, ctx.getPrincipal()
.getAuthenticationLevel(), normName, attr.getID(), null, SEARCH_ATTRVAL_PERMS, tuples, entry, null ) )
{
result.getAttributes().remove( attr.getID() );
if ( attr.size() == 0 )
{
result.getAttributes().remove( attr.getID() );
}
continue;
}
// attribute type scope is ok now let's determine value level scope
for ( int ii = 0; ii < attr.size(); ii++ )
{
if ( !engine.hasPermission( invocation.getProxy(), userGroups, userDn, ctx.getPrincipal()
.getAuthenticationLevel(), normName, attr.getID(), attr.get( ii ), SEARCH_ATTRVAL_PERMS, tuples,
entry, null ) )
{
attr.remove( ii );
if ( ii > 0 )
{
ii--;
}
}
}
}
return true;
}
/**
* WARNING: create one of these filters fresh every time for each new search.
*/
class AuthorizationFilter implements SearchResultFilter
{
public boolean accept( Invocation invocation, SearchResult result, SearchControls controls )
throws NamingException
{
LdapDN normName = parseNormalized( result.getName() );
return filter( invocation, normName, result );
}
}
}