/*
|
* CDDL HEADER START
|
*
|
* The contents of this file are subject to the terms of the
|
* Common Development and Distribution License, Version 1.0 only
|
* (the "License"). You may not use this file except in compliance
|
* with the License.
|
*
|
* You can obtain a copy of the license at
|
* trunk/opends/resource/legal-notices/OpenDS.LICENSE
|
* or https://OpenDS.dev.java.net/OpenDS.LICENSE.
|
* See the License for the specific language governing permissions
|
* and limitations under the License.
|
*
|
* When distributing Covered Code, include this CDDL HEADER in each
|
* file and include the License file at
|
* trunk/opends/resource/legal-notices/OpenDS.LICENSE. If applicable,
|
* add the following below this CDDL HEADER, with the fields enclosed
|
* by brackets "[]" replaced with your own identifying information:
|
* Portions Copyright [yyyy] [name of copyright owner]
|
*
|
* CDDL HEADER END
|
*
|
*
|
* Copyright 2008-2010 Sun Microsystems, Inc.
|
* Portions Copyright 2011-2013 ForgeRock AS
|
* Portions Copyright 2013 Manuel Gaupp
|
*/
|
package org.opends.server.authorization.dseecompat;
|
|
|
|
import static org.opends.messages.AccessControlMessages.*;
|
import static org.opends.server.authorization.dseecompat.Aci.*;
|
import static org.opends.server.config.ConfigConstants.*;
|
import static org.opends.server.loggers.ErrorLogger.*;
|
import static org.opends.server.loggers.debug.DebugLogger.*;
|
import static org.opends.server.schema.SchemaConstants.*;
|
import static org.opends.server.util.ServerConstants.*;
|
import static org.opends.server.util.StaticUtils.*;
|
|
import java.util.LinkedHashSet;
|
import java.util.LinkedList;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.SortedSet;
|
import java.util.concurrent.locks.Lock;
|
|
import org.opends.messages.Message;
|
import org.opends.server.admin.std.server.DseeCompatAccessControlHandlerCfg;
|
import org.opends.server.api.AccessControlHandler;
|
import org.opends.server.api.ClientConnection;
|
import org.opends.server.api.ConfigHandler;
|
import org.opends.server.backends.jeb.EntryContainer;
|
import org.opends.server.config.ConfigException;
|
import org.opends.server.controls.GetEffectiveRightsRequestControl;
|
import org.opends.server.core.DirectoryServer;
|
import org.opends.server.core.ExtendedOperation;
|
import org.opends.server.core.SearchOperation;
|
import org.opends.server.loggers.debug.DebugTracer;
|
import org.opends.server.protocols.internal.InternalClientConnection;
|
import org.opends.server.protocols.internal.InternalSearchOperation;
|
import org.opends.server.protocols.ldap.LDAPControl;
|
import org.opends.server.types.*;
|
import org.opends.server.workflowelement.localbackend.*;
|
|
|
|
/**
|
* The AciHandler class performs the main processing for the dseecompat
|
* package.
|
*/
|
public final class AciHandler extends
|
AccessControlHandler<DseeCompatAccessControlHandlerCfg>
|
{
|
/**
|
* String used to indicate that the evaluating ACI had a all
|
* operational attributes targetattr match (targetattr="+").
|
*/
|
public static final String ALL_OP_ATTRS_MATCHED = "allOpAttrsMatched";
|
|
/**
|
* String used to indicate that the evaluating ACI had a all user
|
* attributes targetattr match (targetattr="*").
|
*/
|
public static final String ALL_USER_ATTRS_MATCHED =
|
"allUserAttrsMatched";
|
|
/**
|
* String used to save the original authorization entry in an
|
* operation attachment if a proxied authorization control was seen.
|
*/
|
public static final String ORIG_AUTH_ENTRY = "origAuthorizationEntry";
|
|
/**
|
* Attribute type corresponding to "aci" attribute.
|
*/
|
static AttributeType aciType;
|
|
/**
|
* Attribute type corresponding to global "ds-cfg-global-aci"
|
* attribute.
|
*/
|
static AttributeType globalAciType;
|
|
/**
|
* Attribute type corresponding to "debugsearchindex" attribute.
|
*/
|
private static AttributeType debugSearchIndex;
|
|
/*
|
* DN corresponding to "debugsearchindex" attribute type.
|
*/
|
private static DN debugSearchIndexDN;
|
|
/**
|
* Attribute type corresponding to the "ref" attribute type. Used in
|
* the search reference access check.
|
*/
|
private static AttributeType refAttrType;
|
|
/**
|
* The tracer object for the debug logger.
|
*/
|
private static final DebugTracer TRACER = getTracer();
|
|
static
|
{
|
initStatics();
|
}
|
|
|
|
// We initialize these for each new AciHandler so that we can clear
|
// out the stale references that can occur during an in-core restart.
|
private static void initStatics()
|
{
|
if ((aciType = DirectoryServer.getAttributeType("aci")) == null)
|
{
|
aciType = DirectoryServer.getDefaultAttributeType("aci");
|
}
|
|
if ((globalAciType =
|
DirectoryServer.getAttributeType(ATTR_AUTHZ_GLOBAL_ACI)) == null)
|
{
|
globalAciType =
|
DirectoryServer
|
.getDefaultAttributeType(ATTR_AUTHZ_GLOBAL_ACI);
|
}
|
|
if ((debugSearchIndex =
|
DirectoryServer
|
.getAttributeType(EntryContainer.ATTR_DEBUG_SEARCH_INDEX)) == null)
|
{
|
debugSearchIndex =
|
DirectoryServer
|
.getDefaultAttributeType(EntryContainer.ATTR_DEBUG_SEARCH_INDEX);
|
}
|
|
if ((refAttrType =
|
DirectoryServer.getAttributeType(ATTR_REFERRAL_URL)) == null)
|
{
|
refAttrType =
|
DirectoryServer.getDefaultAttributeType(ATTR_REFERRAL_URL);
|
}
|
try
|
{
|
debugSearchIndexDN = DN.decode("cn=debugsearch");
|
}
|
catch (DirectoryException ex)
|
{
|
// Should never happen.
|
}
|
}
|
|
|
|
/**
|
* The list that holds that ACIs keyed by the DN of the entry holding
|
* the ACI.
|
*/
|
private AciList aciList;
|
|
/**
|
* The listener that handles ACI changes caused by LDAP operations,
|
* ACI decode failure alert logging and backend initialization ACI
|
* list adjustment.
|
*/
|
private AciListenerManager aciListenerMgr;
|
|
|
|
/**
|
* Creates a new DSEE-compatible access control handler.
|
*/
|
public AciHandler()
|
{
|
// No implementation required. All initialization should be done in
|
// the intializeAccessControlHandler method.
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public void filterEntry(Operation operation,
|
SearchResultEntry unfilteredEntry, SearchResultEntry filteredEntry)
|
{
|
AciLDAPOperationContainer operationContainer =
|
new AciLDAPOperationContainer(operation, (ACI_READ), unfilteredEntry);
|
|
// Proxy access check has already been done for this entry in the
|
// maySend method, set the seen flag to true to bypass any proxy
|
// check.
|
operationContainer.setSeenEntry(true);
|
|
boolean skipCheck = skipAccessCheck(operation);
|
if (!skipCheck)
|
{
|
filterEntry(operationContainer, filteredEntry);
|
}
|
|
if (operationContainer.hasGetEffectiveRightsControl())
|
{
|
AciEffectiveRights.addRightsToEntry(this,
|
((SearchOperation) operation).getAttributes(), operationContainer,
|
filteredEntry, skipCheck);
|
}
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override()
|
public void finalizeAccessControlHandler()
|
{
|
aciListenerMgr.finalizeListenerManager();
|
AciEffectiveRights.finalizeOnShutdown();
|
DirectoryServer
|
.deregisterSupportedControl(OID_GET_EFFECTIVE_RIGHTS);
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override()
|
public void initializeAccessControlHandler(
|
DseeCompatAccessControlHandlerCfg configuration)
|
throws ConfigException, InitializationException
|
{
|
initStatics();
|
DN configurationDN = configuration.dn();
|
aciList = new AciList(configurationDN);
|
aciListenerMgr = new AciListenerManager(aciList, configurationDN);
|
processGlobalAcis(configuration);
|
processConfigAcis();
|
DirectoryServer.registerSupportedControl(OID_GET_EFFECTIVE_RIGHTS);
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public boolean isAllowed(DN entryDN, Operation op, Control control)
|
throws DirectoryException
|
{
|
boolean ret;
|
if (!(ret = skipAccessCheck(op)))
|
{
|
Entry e = new Entry(entryDN, null, null, null);
|
AciLDAPOperationContainer operationContainer =
|
new AciLDAPOperationContainer(op, e, control,
|
(ACI_READ | ACI_CONTROL));
|
ret = accessAllowed(operationContainer);
|
}
|
if (control.getOID().equals(OID_PROXIED_AUTH_V2)
|
|| control.getOID().equals(OID_PROXIED_AUTH_V1))
|
{
|
if (ret)
|
{
|
op.setAttachment(ORIG_AUTH_ENTRY, op.getAuthorizationEntry());
|
}
|
}
|
else if (control.getOID().equals(OID_GET_EFFECTIVE_RIGHTS))
|
{
|
if (ret)
|
{
|
GetEffectiveRightsRequestControl getEffectiveRightsControl;
|
if (control instanceof LDAPControl)
|
{
|
getEffectiveRightsControl = GetEffectiveRightsRequestControl.DECODER
|
.decode(control.isCritical(), ((LDAPControl) control).getValue());
|
}
|
else
|
{
|
getEffectiveRightsControl =
|
(GetEffectiveRightsRequestControl) control;
|
}
|
op.setAttachment(OID_GET_EFFECTIVE_RIGHTS, getEffectiveRightsControl);
|
}
|
}
|
return ret;
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public boolean isAllowed(ExtendedOperation operation)
|
{
|
boolean ret;
|
if (!(ret = skipAccessCheck(operation)))
|
{
|
Entry e =
|
new Entry(operation.getAuthorizationDN(), null, null, null);
|
AciLDAPOperationContainer operationContainer =
|
new AciLDAPOperationContainer(operation, e,
|
(ACI_READ | ACI_EXT_OP));
|
ret = accessAllowed(operationContainer);
|
}
|
return ret;
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public boolean isAllowed(LocalBackendAddOperation operation)
|
throws DirectoryException
|
{
|
AciLDAPOperationContainer operationContainer =
|
new AciLDAPOperationContainer(operation, ACI_ADD);
|
boolean ret = isAllowed(operationContainer, operation);
|
|
// LDAP add needs a verify ACI syntax step in case any
|
// "aci" attribute types are being added.
|
if (ret)
|
{
|
ret =
|
verifySyntax(operation.getEntryToAdd(), operation,
|
operationContainer.getClientDN());
|
}
|
return ret;
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public boolean isAllowed(LocalBackendBindOperation bindOperation)
|
{
|
// Not planned to be implemented.
|
return true;
|
}
|
|
|
|
/**
|
* Check access on compare operations. Note that the attribute type is
|
* unavailable at this time, so this method partially parses the raw
|
* attribute string to get the base attribute type. Options are
|
* ignored.
|
*
|
* @param operation
|
* The compare operation to check access on.
|
* @return True if access is allowed.
|
*/
|
@Override
|
public boolean isAllowed(LocalBackendCompareOperation operation)
|
{
|
AciLDAPOperationContainer operationContainer =
|
new AciLDAPOperationContainer(operation, ACI_COMPARE);
|
String baseName;
|
String rawAttributeType = operation.getRawAttributeType();
|
int semicolonPosition = rawAttributeType.indexOf(';');
|
if (semicolonPosition > 0)
|
{
|
baseName =
|
toLowerCase(rawAttributeType.substring(0, semicolonPosition));
|
}
|
else
|
{
|
baseName = toLowerCase(rawAttributeType);
|
}
|
AttributeType attributeType;
|
if ((attributeType = DirectoryServer.getAttributeType(baseName)) == null)
|
{
|
attributeType = DirectoryServer.getDefaultAttributeType(baseName);
|
}
|
AttributeValue attributeValue =
|
AttributeValues.create(attributeType, operation
|
.getAssertionValue());
|
operationContainer.setCurrentAttributeType(attributeType);
|
operationContainer.setCurrentAttributeValue(attributeValue);
|
return isAllowed(operationContainer, operation);
|
}
|
|
|
|
/**
|
* Check access on delete operations.
|
*
|
* @param operation
|
* The delete operation to check access on.
|
* @return True if access is allowed.
|
*/
|
@Override
|
public boolean isAllowed(LocalBackendDeleteOperation operation)
|
{
|
AciLDAPOperationContainer operationContainer =
|
new AciLDAPOperationContainer(operation, ACI_DELETE);
|
return isAllowed(operationContainer, operation);
|
}
|
|
|
|
/**
|
* Checks access on a modifyDN operation.
|
*
|
* @param operation
|
* The modifyDN operation to check access on.
|
* @return True if access is allowed.
|
*/
|
@Override
|
public boolean isAllowed(LocalBackendModifyDNOperation operation)
|
{
|
boolean ret = true;
|
DN newSuperiorDN;
|
RDN oldRDN = operation.getOriginalEntry().getDN().getRDN();
|
RDN newRDN = operation.getNewRDN();
|
if (!skipAccessCheck(operation))
|
{
|
// If this is a modifyDN move to a new superior, then check if the
|
// superior DN has import accesss.
|
if ((newSuperiorDN = operation.getNewSuperior()) != null)
|
{
|
try
|
{
|
ret = aciCheckSuperiorEntry(newSuperiorDN, operation);
|
}
|
catch (DirectoryException ex)
|
{
|
ret = false;
|
}
|
}
|
// Perform the RDN access checks.
|
if (ret)
|
{
|
ret = aciCheckRDNs(operation, oldRDN, newRDN);
|
}
|
|
// If this is a modifyDN move to a new superior, then check if the
|
// original entry DN has export access.
|
if (ret && (newSuperiorDN != null))
|
{
|
AciLDAPOperationContainer operationContainer =
|
new AciLDAPOperationContainer(operation, (ACI_EXPORT),
|
operation.getOriginalEntry());
|
// The RDNs are not equal, skip the proxy check since it was
|
// already performed in the aciCheckRDNs call above.
|
boolean rdnEquals = oldRDN.equals(newRDN);
|
if (!rdnEquals)
|
{
|
operationContainer.setSeenEntry(true);
|
}
|
ret = accessAllowed(operationContainer);
|
}
|
}
|
return ret;
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public boolean isAllowed(LocalBackendModifyOperation operation)
|
throws DirectoryException
|
{
|
AciLDAPOperationContainer operationContainer =
|
new AciLDAPOperationContainer(operation, ACI_NULL);
|
return aciCheckMods(operationContainer, operation,
|
skipAccessCheck(operation));
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public boolean isAllowed(LocalBackendSearchOperation searchOperation)
|
{
|
// Not planned to be implemented.
|
return true;
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public boolean isAllowed(Operation operation, Entry entry,
|
SearchFilter filter) throws DirectoryException
|
{
|
if (skipAccessCheck(operation))
|
{
|
return true;
|
}
|
else
|
{
|
AciLDAPOperationContainer operationContainer =
|
new AciLDAPOperationContainer(operation, (ACI_READ), entry);
|
return testFilter(operationContainer, filter);
|
}
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public boolean mayProxy(Entry proxyUser, Entry proxiedUser,
|
Operation op)
|
{
|
boolean ret;
|
if (!(ret = skipAccessCheck(proxyUser)))
|
{
|
AuthenticationInfo authInfo =
|
new AuthenticationInfo(proxyUser, DirectoryServer
|
.isRootDN(proxyUser.getDN()));
|
AciLDAPOperationContainer operationContainer =
|
new AciLDAPOperationContainer(op, proxiedUser, authInfo,
|
ACI_PROXY);
|
ret = accessAllowedEntry(operationContainer);
|
}
|
return ret;
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public boolean maySend(DN dn, Operation operation,
|
SearchResultReference reference)
|
{
|
boolean ret;
|
if (!(ret = skipAccessCheck(operation)))
|
{
|
Entry e = new Entry(dn, null, null, null);
|
AttributeBuilder builder =
|
new AttributeBuilder(refAttrType, ATTR_REFERRAL_URL);
|
List<String> URLStrings = reference.getReferralURLs();
|
|
// Load the values, a bind rule might want to evaluate them.
|
for (String URLString : URLStrings)
|
{
|
builder.add(AttributeValues.create(refAttrType, URLString));
|
}
|
|
e.addAttribute(builder.toAttribute(), null);
|
SearchResultEntry se = new SearchResultEntry(e);
|
AciLDAPOperationContainer operationContainer =
|
new AciLDAPOperationContainer(operation, (ACI_READ), se);
|
operationContainer.setCurrentAttributeType(refAttrType);
|
ret = accessAllowed(operationContainer);
|
}
|
return ret;
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public boolean maySend(Operation operation, SearchResultEntry entry)
|
{
|
if (skipAccessCheck(operation))
|
{
|
return true;
|
}
|
|
AciLDAPOperationContainer operationContainer =
|
new AciLDAPOperationContainer(operation, (ACI_SEARCH), entry);
|
|
// Pre/post read controls are associated with other types of operation.
|
if (operation instanceof SearchOperation)
|
{
|
try
|
{
|
if (!testFilter(operationContainer,
|
((SearchOperation) operation).getFilter()))
|
{
|
return false;
|
}
|
}
|
catch (DirectoryException ex)
|
{
|
return false;
|
}
|
}
|
|
operationContainer.clearEvalAttributes(ACI_NULL);
|
operationContainer.setRights(ACI_READ);
|
|
if (!accessAllowedEntry(operationContainer))
|
{
|
return false;
|
}
|
|
if (!operationContainer.hasEvalUserAttributes())
|
{
|
operation.setAttachment(ALL_USER_ATTRS_MATCHED, ALL_USER_ATTRS_MATCHED);
|
}
|
|
if (!operationContainer.hasEvalOpAttributes())
|
{
|
operation.setAttachment(ALL_OP_ATTRS_MATCHED, ALL_OP_ATTRS_MATCHED);
|
}
|
|
return true;
|
}
|
|
|
|
/**
|
* Check access using the specified container. This container will
|
* have all of the information to gather applicable ACIs and perform
|
* evaluation on them.
|
*
|
* @param container
|
* An ACI operation container which has all of the
|
* information needed to check access.
|
* @return True if access is allowed.
|
*/
|
boolean accessAllowed(AciContainer container)
|
{
|
DN dn = container.getResourceEntry().getDN();
|
// For ACI_WRITE_ADD and ACI_WRITE_DELETE set the ACI_WRITE
|
// right.
|
if (container.hasRights(ACI_WRITE_ADD)
|
|| container.hasRights(ACI_WRITE_DELETE))
|
{
|
container.setRights(container.getRights() | ACI_WRITE);
|
}
|
// Check if the ACI_SELF right needs to be set (selfwrite right).
|
// Only done if the right is ACI_WRITE, an attribute value is set
|
// and that attribute value is a DN.
|
if ((container.getCurrentAttributeValue() != null)
|
&& (container.hasRights(ACI_WRITE))
|
&& (isAttributeDN(container.getCurrentAttributeType())))
|
{
|
String DNString = null;
|
try
|
{
|
DNString =
|
container.getCurrentAttributeValue().getValue().toString();
|
DN tmpDN = DN.decode(DNString);
|
// Have a valid DN, compare to clientDN to see if the ACI_SELF
|
// right should be set.
|
if (tmpDN.equals(container.getClientDN()))
|
{
|
container.setRights(container.getRights() | ACI_SELF);
|
}
|
}
|
catch (DirectoryException ex)
|
{
|
// Log a message and keep going.
|
Message message = WARN_ACI_NOT_VALID_DN.get(DNString);
|
logError(message);
|
}
|
}
|
|
// Check proxy authorization only if the entry has not already been
|
// processed (working on a new entry). If working on a new entry,
|
// then only do a proxy check if the right is not set to ACI_PROXY
|
// and the proxied authorization control has been decoded.
|
if (!container.hasSeenEntry())
|
{
|
if (container.isProxiedAuthorization()
|
&& !container.hasRights(ACI_PROXY)
|
&& !container.hasRights(ACI_SKIP_PROXY_CHECK))
|
{
|
int currentRights = container.getRights();
|
// Save the current rights so they can be put back if on
|
// success.
|
container.setRights(ACI_PROXY);
|
// Switch to the original authorization entry, not the proxied
|
// one.
|
container.useOrigAuthorizationEntry(true);
|
if (!accessAllowed(container))
|
{
|
return false;
|
}
|
// Access is ok, put the original rights back.
|
container.setRights(currentRights);
|
// Put the proxied authorization entry back to the current
|
// authorization entry.
|
container.useOrigAuthorizationEntry(false);
|
}
|
// Set the seen flag so proxy processing is not performed for this
|
// entry again.
|
container.setSeenEntry(true);
|
}
|
|
/*
|
* First get all allowed candidate ACIs.
|
*/
|
LinkedList<Aci> candidates = aciList.getCandidateAcis(dn);
|
/*
|
* Create an applicable list of ACIs by target matching each
|
* candidate ACI against the container's target match view.
|
*/
|
createApplicableList(candidates, container);
|
/*
|
* Evaluate the applicable list.
|
*/
|
boolean ret = testApplicableLists(container);
|
// Build summary string if doing geteffectiverights eval.
|
if (container.isGetEffectiveRightsEval())
|
{
|
AciEffectiveRights.createSummary(container, ret, "main");
|
}
|
return ret;
|
}
|
|
|
|
/*
|
* TODO Evaluate performance of this method. TODO Evaluate security
|
* concerns of this method. Logic from this method taken almost
|
* directly from DS6 implementation. I find the work done in the
|
* accessAllowedEntry method, particularly with regard to the entry
|
* test evaluation, to be very confusing and potentially pretty
|
* inefficient. I'm also concerned that the "return "true" inside the
|
* for loop could potentially allow access when it should be denied.
|
*/
|
|
/**
|
* Check if access is allowed on an entry. Access is checked by
|
* iterating through each attribute of an entry, starting with the
|
* "objectclass" attribute type. If access is allowed on the entry
|
* based on one of it's attribute types, then a possible second access
|
* check is performed. This second check is only performed if an entry
|
* test ACI was found during the earlier successful access check. An
|
* entry test ACI has no "targetattrs" keyword, so allowing access
|
* based on an attribute type only would be incorrect.
|
*
|
* @param container
|
* ACI search container containing all of the information
|
* needed to check access.
|
* @return True if access is allowed.
|
*/
|
boolean accessAllowedEntry(AciLDAPOperationContainer container)
|
{
|
boolean ret = false;
|
// set flag that specifies this is the first attribute evaluated
|
// in the entry
|
container.setIsFirstAttribute(true);
|
List<AttributeType> typeList =
|
getAllAttrs(container.getResourceEntry());
|
for (AttributeType attrType : typeList)
|
{
|
container.setCurrentAttributeType(attrType);
|
/*
|
* Check if access is allowed. If true, then check to see if an
|
* entry test rule was found (no targetattrs) during target match
|
* evaluation. If such a rule was found, set the current attribute
|
* type to "null" and check access again so that rule is applied.
|
*/
|
if (accessAllowed(container))
|
{
|
if (container.hasEntryTestRule())
|
{
|
container.setCurrentAttributeType(null);
|
if (!accessAllowed(container))
|
{
|
/*
|
* If we failed because of a deny permission-bind rule, we
|
* need to stop and return false.
|
*/
|
if (container.isDenyEval())
|
{
|
return false;
|
}
|
/*
|
* If we failed because there was no explicit allow rule,
|
* then we grant implicit access to the entry.
|
*/
|
}
|
}
|
return true;
|
}
|
}
|
return ret;
|
}
|
|
|
|
/**
|
* Performs an access check against all of the attributes of an entry. The
|
* attributes that fail access are removed from the entry. This method
|
* performs the processing needed for the filterEntry method processing.
|
*
|
* @param container
|
* The search or compare container which has all of the information
|
* needed to filter the attributes for this entry.
|
* @param filteredEntry
|
* The partially filtered search result entry being returned to the
|
* client.
|
*/
|
private void filterEntry(AciLDAPOperationContainer container,
|
Entry filteredEntry)
|
{
|
List<AttributeType> typeList = getAllAttrs(filteredEntry);
|
for (AttributeType attrType : typeList)
|
{
|
if (container.hasAllUserAttributes() && !attrType.isOperational())
|
{
|
continue;
|
}
|
if (container.hasAllOpAttributes() && attrType.isOperational())
|
{
|
continue;
|
}
|
container.setCurrentAttributeType(attrType);
|
if (!accessAllowed(container))
|
{
|
filteredEntry.removeAttribute(attrType);
|
}
|
}
|
}
|
|
|
|
/**
|
* Checks to see if a LDAP modification is allowed access.
|
*
|
* @param container
|
* The structure containing the LDAP modifications
|
* @param operation
|
* The operation to check modify privileges on. operation to
|
* check and the evaluation context to apply the check
|
* against.
|
* @param skipAccessCheck
|
* True if access checking should be skipped.
|
* @return True if access is allowed.
|
* @throws DirectoryException
|
* If a modified ACI could not be decoded.
|
*/
|
private boolean aciCheckMods(AciLDAPOperationContainer container,
|
LocalBackendModifyOperation operation, boolean skipAccessCheck)
|
throws DirectoryException
|
{
|
Entry resourceEntry = container.getResourceEntry();
|
DN dn = resourceEntry.getDN();
|
List<Modification> modifications = operation.getModifications();
|
|
for (Modification m : modifications)
|
{
|
Attribute modAttr = m.getAttribute();
|
AttributeType modAttrType = modAttr.getAttributeType();
|
|
if (modAttrType.equals(aciType))
|
{
|
/*
|
* Check that the operation has modify privileges if it contains
|
* an "aci" attribute type.
|
*/
|
if (!operation.getClientConnection().hasPrivilege(
|
Privilege.MODIFY_ACL, operation))
|
{
|
Message message =
|
INFO_ACI_MODIFY_FAILED_PRIVILEGE.get(String
|
.valueOf(container.getResourceDN()), String
|
.valueOf(container.getClientDN()));
|
logError(message);
|
return false;
|
}
|
}
|
// This access check handles the case where all attributes of this
|
// type are being replaced or deleted. If only a subset is being
|
// deleted than this access check is skipped.
|
ModificationType modType = m.getModificationType();
|
if (((modType == ModificationType.DELETE) && modAttr.isEmpty())
|
|| ((modType == ModificationType.REPLACE)
|
|| (modType == ModificationType.INCREMENT)))
|
{
|
/*
|
* Check if we have rights to delete all values of an attribute
|
* type in the resource entry.
|
*/
|
if (resourceEntry.hasAttribute(modAttrType))
|
{
|
container.setCurrentAttributeType(modAttrType);
|
List<Attribute> attrList =
|
resourceEntry.getAttribute(modAttrType, modAttr
|
.getOptions());
|
if (attrList != null)
|
{
|
for (Attribute a : attrList)
|
{
|
for (AttributeValue v : a)
|
{
|
container.setCurrentAttributeValue(v);
|
container.setRights(ACI_WRITE_DELETE);
|
if (!skipAccessCheck && !accessAllowed(container))
|
{
|
return false;
|
}
|
}
|
}
|
}
|
}
|
}
|
|
if (!modAttr.isEmpty())
|
{
|
for (AttributeValue v : modAttr)
|
{
|
container.setCurrentAttributeType(modAttrType);
|
switch (m.getModificationType())
|
{
|
case ADD:
|
case REPLACE:
|
container.setCurrentAttributeValue(v);
|
container.setRights(ACI_WRITE_ADD);
|
if (!skipAccessCheck && !accessAllowed(container))
|
{
|
return false;
|
}
|
break;
|
case DELETE:
|
container.setCurrentAttributeValue(v);
|
container.setRights(ACI_WRITE_DELETE);
|
if (!skipAccessCheck && !accessAllowed(container))
|
{
|
return false;
|
}
|
break;
|
case INCREMENT:
|
Entry modifiedEntry = operation.getModifiedEntry();
|
List<Attribute> modifiedAttrs =
|
modifiedEntry.getAttribute(modAttrType, modAttr
|
.getOptions());
|
if (modifiedAttrs != null)
|
{
|
for (Attribute attr : modifiedAttrs)
|
{
|
for (AttributeValue val : attr)
|
{
|
container.setCurrentAttributeValue(val);
|
container.setRights(ACI_WRITE_ADD);
|
if (!skipAccessCheck && !accessAllowed(container))
|
{
|
return false;
|
}
|
}
|
}
|
}
|
break;
|
}
|
/*
|
* Check if the modification type has an "aci" attribute type.
|
* If so, check the syntax of that attribute value. Fail the
|
* the operation if the syntax check fails.
|
*/
|
if (modAttrType.equals(aciType)
|
|| modAttrType.equals(globalAciType))
|
{
|
try
|
{
|
// A global ACI needs a NULL DN, not the DN of the
|
// modification.
|
if (modAttrType.equals(globalAciType))
|
{
|
dn = DN.nullDN();
|
}
|
Aci.decode(v.getValue(), dn);
|
}
|
catch (AciException ex)
|
{
|
Message message =
|
WARN_ACI_MODIFY_FAILED_DECODE.get(String.valueOf(dn),
|
ex.getMessage());
|
throw new DirectoryException(
|
ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
|
}
|
}
|
}
|
}
|
}
|
return true;
|
}
|
|
|
|
/**
|
* Perform all needed RDN checks for the modifyDN operation. The old
|
* RDN is not equal to the new RDN. The access checks are: - Verify
|
* WRITE access to the original entry. - Verfiy WRITE_ADD access on
|
* each RDN component of the new RDN. The WRITE_ADD access is used
|
* because this access could be restricted by the targattrfilters
|
* keyword. - If the deleteOLDRDN flag is set, verify WRITE_DELETE
|
* access on the old RDN. The WRITE_DELETE access is used because this
|
* access could be restricted by the targattrfilters keyword.
|
*
|
* @param operation
|
* The ModifyDN operation class containing information to
|
* check access on.
|
* @param oldRDN
|
* The old RDN component.
|
* @param newRDN
|
* The new RDN component.
|
* @return True if access is allowed.
|
*/
|
private boolean aciCheckRDNs(LocalBackendModifyDNOperation operation,
|
RDN oldRDN, RDN newRDN)
|
{
|
boolean ret;
|
|
AciLDAPOperationContainer operationContainer =
|
new AciLDAPOperationContainer(operation, (ACI_WRITE), operation
|
.getOriginalEntry());
|
ret = accessAllowed(operationContainer);
|
if (ret)
|
{
|
ret = checkRDN(ACI_WRITE_ADD, newRDN, operationContainer);
|
}
|
if (ret && operation.deleteOldRDN())
|
{
|
ret = checkRDN(ACI_WRITE_DELETE, oldRDN, operationContainer);
|
}
|
return ret;
|
}
|
|
|
|
/**
|
* Check access on the new superior entry if it exists. If the entry
|
* does not exist or the DN cannot be locked then false is returned.
|
*
|
* @param superiorDN
|
* The DN of the new superior entry.
|
* @param op
|
* The modifyDN operation to check access on.
|
* @return True if access is granted to the new superior entry.
|
* @throws DirectoryException
|
* If a problem occurs while trying to retrieve the new
|
* superior entry.
|
*/
|
private boolean aciCheckSuperiorEntry(DN superiorDN,
|
LocalBackendModifyDNOperation op) throws DirectoryException
|
{
|
boolean ret = false;
|
final Lock entryLock = LockManager.lockRead(superiorDN);
|
if (entryLock == null)
|
{
|
Message message =
|
WARN_ACI_HANDLER_CANNOT_LOCK_NEW_SUPERIOR_USER.get(String
|
.valueOf(superiorDN));
|
logError(message);
|
return false;
|
}
|
try
|
{
|
Entry superiorEntry = DirectoryServer.getEntry(superiorDN);
|
if (superiorEntry != null)
|
{
|
AciLDAPOperationContainer operationContainer =
|
new AciLDAPOperationContainer(op, (ACI_IMPORT),
|
superiorEntry);
|
ret = accessAllowed(operationContainer);
|
}
|
}
|
finally
|
{
|
LockManager.unlock(superiorDN, entryLock);
|
}
|
return ret;
|
}
|
|
|
|
/**
|
* Check access on each attribute-value pair component of the
|
* specified RDN. There may be more than one attribute-value pair if
|
* the RDN is multi-valued.
|
*
|
* @param right
|
* The access right to check for.
|
* @param rdn
|
* The RDN to examine the attribute-value pairs of.
|
* @param container
|
* The container containing the information needed to
|
* evaluate the specified RDN.
|
* @return True if access is allowed for all attribute-value pairs.
|
*/
|
private boolean checkRDN(int right, RDN rdn, AciContainer container)
|
{
|
boolean ret = false;
|
int numAVAs = rdn.getNumValues();
|
container.setRights(right);
|
for (int i = 0; i < numAVAs; i++)
|
{
|
AttributeType type = rdn.getAttributeType(i);
|
AttributeValue value = rdn.getAttributeValue(i);
|
container.setCurrentAttributeType(type);
|
container.setCurrentAttributeValue(value);
|
if (!(ret = accessAllowed(container)))
|
{
|
break;
|
}
|
}
|
return ret;
|
}
|
|
|
|
/**
|
* Creates the allow and deny ACI lists based on the provided target
|
* match context. These lists are stored in the evaluation context.
|
*
|
* @param candidates
|
* List of all possible ACI candidates.
|
* @param targetMatchCtx
|
* Target matching context to use for testing each ACI.
|
*/
|
private void createApplicableList(LinkedList<Aci> candidates,
|
AciTargetMatchContext targetMatchCtx)
|
{
|
LinkedList<Aci> denys = new LinkedList<Aci>();
|
LinkedList<Aci> allows = new LinkedList<Aci>();
|
for (Aci aci : candidates)
|
{
|
if (Aci.isApplicable(aci, targetMatchCtx))
|
{
|
if (aci.hasAccessType(EnumAccessType.DENY))
|
{
|
denys.add(aci);
|
}
|
if (aci.hasAccessType(EnumAccessType.ALLOW))
|
{
|
allows.add(aci);
|
}
|
}
|
if (targetMatchCtx.getTargAttrFiltersMatch())
|
{
|
targetMatchCtx.setTargAttrFiltersMatch(false);
|
}
|
}
|
targetMatchCtx.setAllowList(allows);
|
targetMatchCtx.setDenyList(denys);
|
}
|
|
|
|
/**
|
* Gathers all of the attribute types in an entry along with the
|
* "objectclass" attribute type in a List. The "objectclass" attribute
|
* is added to the list first so it is evaluated first.
|
*
|
* @param e
|
* Entry to gather the attributes for.
|
* @return List containing the attribute types.
|
*/
|
private List<AttributeType> getAllAttrs(Entry e)
|
{
|
Map<AttributeType, List<Attribute>> attrMap = e.getUserAttributes();
|
Map<AttributeType, List<Attribute>> opAttrMap =
|
e.getOperationalAttributes();
|
List<AttributeType> typeList = new LinkedList<AttributeType>();
|
Attribute attr = e.getObjectClassAttribute();
|
/*
|
* When a search is not all attributes returned, the "objectclass"
|
* attribute type is missing from the entry.
|
*/
|
if (attr != null)
|
{
|
AttributeType ocType = attr.getAttributeType();
|
typeList.add(ocType);
|
}
|
typeList.addAll(attrMap.keySet());
|
typeList.addAll(opAttrMap.keySet());
|
return typeList;
|
}
|
|
|
|
/**
|
* Check access using the accessAllowed method. The LDAP add, compare,
|
* modify and delete operations use this function. The other supported
|
* LDAP operations have more specialized checks.
|
*
|
* @param operationContainer
|
* The container containing the information needed to
|
* evaluate this operation.
|
* @param operation
|
* The operation being evaluated.
|
* @return True if this operation is allowed access.
|
*/
|
private boolean isAllowed(
|
AciLDAPOperationContainer operationContainer, Operation operation)
|
{
|
return skipAccessCheck(operation)
|
|| accessAllowed(operationContainer);
|
}
|
|
|
|
/**
|
* Check if the specified attribute type is a DN by checking if its
|
* syntax OID is equal to the DN syntax OID.
|
*
|
* @param attribute
|
* The attribute type to check.
|
* @return True if the attribute type syntax OID is equal to a DN
|
* syntax OID.
|
*/
|
private boolean isAttributeDN(AttributeType attribute)
|
{
|
return (attribute.getSyntaxOID().equals(SYNTAX_DN_OID));
|
}
|
|
|
|
/**
|
* Process all ACIs under the "cn=config" naming context and adds them
|
* to the ACI list cache. It also logs messages about the number of
|
* ACIs added to the cache. This method is called once at startup. It
|
* will put the server in lockdown mode if needed.
|
*
|
* @throws InitializationException
|
* If there is an error searching for the ACIs in the naming
|
* context.
|
*/
|
private void processConfigAcis() throws InitializationException
|
{
|
LinkedHashSet<String> requestAttrs = new LinkedHashSet<String>(1);
|
requestAttrs.add("aci");
|
LinkedList<Message> failedACIMsgs = new LinkedList<Message>();
|
InternalClientConnection conn =
|
InternalClientConnection.getRootConnection();
|
|
ConfigHandler configBackend = DirectoryServer.getConfigHandler();
|
for (DN baseDN : configBackend.getBaseDNs())
|
{
|
try
|
{
|
if (! configBackend.entryExists(baseDN))
|
{
|
continue;
|
}
|
}
|
catch (Exception e)
|
{
|
if (debugEnabled())
|
{
|
TRACER.debugCaught(DebugLogLevel.ERROR, e);
|
}
|
|
// FIXME -- Is there anything that we need to do here?
|
continue;
|
}
|
|
try {
|
InternalSearchOperation internalSearch = new InternalSearchOperation(
|
conn, InternalClientConnection.nextOperationID(),
|
InternalClientConnection.nextMessageID(),
|
null, baseDN, SearchScope.WHOLE_SUBTREE,
|
DereferencePolicy.NEVER_DEREF_ALIASES, 0, 0, false,
|
SearchFilter.createFilterFromString("aci=*"), requestAttrs, null);
|
LocalBackendSearchOperation localSearch =
|
new LocalBackendSearchOperation(internalSearch);
|
|
configBackend.search(localSearch);
|
|
if (!internalSearch.getSearchEntries().isEmpty())
|
{
|
int validAcis =
|
aciList.addAci(internalSearch.getSearchEntries(), failedACIMsgs);
|
if (!failedACIMsgs.isEmpty())
|
{
|
aciListenerMgr.logMsgsSetLockDownMode(failedACIMsgs);
|
}
|
Message message =
|
INFO_ACI_ADD_LIST_ACIS.get(Integer.toString(validAcis),
|
String.valueOf(baseDN));
|
logError(message);
|
}
|
}
|
catch (Exception e)
|
{
|
Message message = INFO_ACI_HANDLER_FAIL_PROCESS_ACI.get();
|
throw new InitializationException(message, e);
|
}
|
}
|
}
|
|
|
|
/**
|
* Process all global ACI attribute types found in the configuration
|
* entry and adds them to that ACI list cache. It also logs messages
|
* about the number of ACI attribute types added to the cache. This
|
* method is called once at startup. It also will put the server into
|
* lockdown mode if needed.
|
*
|
* @param configuration
|
* The config handler containing the ACI configuration
|
* information.
|
* @throws InitializationException
|
* If there is an error reading the global ACIs from the
|
* configuration entry.
|
*/
|
private void processGlobalAcis(
|
DseeCompatAccessControlHandlerCfg configuration)
|
throws InitializationException
|
{
|
SortedSet<Aci> globalAcis = configuration.getGlobalACI();
|
try
|
{
|
if (globalAcis != null)
|
{
|
aciList.addAci(DN.nullDN(), globalAcis);
|
Message message =
|
INFO_ACI_ADD_LIST_GLOBAL_ACIS.get(Integer
|
.toString(globalAcis.size()));
|
logError(message);
|
}
|
}
|
catch (Exception e)
|
{
|
if (debugEnabled())
|
{
|
TRACER.debugCaught(DebugLogLevel.ERROR, e);
|
}
|
Message message =
|
INFO_ACI_HANDLER_FAIL_PROCESS_GLOBAL_ACI.get(String
|
.valueOf(configuration.dn()));
|
throw new InitializationException(message, e);
|
}
|
}
|
|
|
|
/**
|
* Check to see if the specified entry has the specified privilege.
|
*
|
* @param e
|
* The entry to check privileges on.
|
* @return {@code true} if the entry has the specified privilege, or
|
* {@code false} if not.
|
*/
|
private boolean skipAccessCheck(Entry e)
|
{
|
return ClientConnection.hasPrivilege(e, Privilege.BYPASS_ACL);
|
}
|
|
|
|
/**
|
* Check to see if the client entry has BYPASS_ACL privileges for this
|
* operation.
|
*
|
* @param operation
|
* The operation to check privileges on.
|
* @return True if access checking can be skipped because the
|
* operation client connection has BYPASS_ACL privileges.
|
*/
|
private boolean skipAccessCheck(Operation operation)
|
{
|
return operation.getClientConnection().hasPrivilege(
|
Privilege.BYPASS_ACL, operation);
|
}
|
|
|
|
/**
|
* Performs the test of the deny and allow access lists using the
|
* provided evaluation context. The deny list is checked first.
|
*
|
* @param evalCtx
|
* The evaluation context to use.
|
* @return True if access is allowed.
|
*/
|
private boolean testApplicableLists(AciEvalContext evalCtx)
|
{
|
EnumEvalResult res;
|
evalCtx.setEvalReason(EnumEvalReason.NO_REASON);
|
LinkedList<Aci> denys = evalCtx.getDenyList();
|
LinkedList<Aci> allows = evalCtx.getAllowList();
|
// If allows list is empty and not doing geteffectiverights return
|
// false.
|
evalCtx.setDenyEval(true);
|
if (allows.isEmpty()
|
&& !(evalCtx.isGetEffectiveRightsEval()
|
&& !evalCtx.hasRights(ACI_SELF) && evalCtx
|
.isTargAttrFilterMatchAciEmpty()))
|
{
|
evalCtx.setEvalReason(EnumEvalReason.NO_ALLOW_ACIS);
|
evalCtx.setDecidingAci(null);
|
return false;
|
}
|
for (Aci denyAci : denys)
|
{
|
res = Aci.evaluate(evalCtx, denyAci);
|
// Failure could be returned if a system limit is hit or
|
// search fails
|
if (res.equals(EnumEvalResult.FAIL))
|
{
|
evalCtx.setEvalReason(EnumEvalReason.EVALUATED_DENY_ACI);
|
evalCtx.setDecidingAci(denyAci);
|
return false;
|
}
|
else if (res.equals(EnumEvalResult.TRUE))
|
{
|
if (evalCtx.isGetEffectiveRightsEval()
|
&& !evalCtx.hasRights(ACI_SELF)
|
&& !evalCtx.isTargAttrFilterMatchAciEmpty())
|
{
|
// Iterate to next only if deny ACI contains a targattrfilters
|
// keyword.
|
if (AciEffectiveRights.setTargAttrAci(evalCtx, denyAci, true))
|
{
|
continue;
|
}
|
evalCtx.setEvalReason(EnumEvalReason.EVALUATED_DENY_ACI);
|
evalCtx.setDecidingAci(denyAci);
|
return false;
|
}
|
else
|
{
|
evalCtx.setEvalReason(EnumEvalReason.EVALUATED_DENY_ACI);
|
evalCtx.setDecidingAci(denyAci);
|
return false;
|
}
|
}
|
}
|
// Now check the allows -- flip the deny flag to false first.
|
evalCtx.setDenyEval(false);
|
for (Aci allowAci : allows)
|
{
|
res = Aci.evaluate(evalCtx, allowAci);
|
if (res.equals(EnumEvalResult.TRUE))
|
{
|
if (evalCtx.isGetEffectiveRightsEval()
|
&& !evalCtx.hasRights(ACI_SELF)
|
&& !evalCtx.isTargAttrFilterMatchAciEmpty())
|
{
|
// Iterate to next only if deny ACI contains a targattrfilters
|
// keyword.
|
if (AciEffectiveRights.setTargAttrAci(evalCtx, allowAci,
|
false))
|
{
|
continue;
|
}
|
evalCtx.setEvalReason(EnumEvalReason.EVALUATED_ALLOW_ACI);
|
evalCtx.setDecidingAci(allowAci);
|
return true;
|
}
|
else
|
{
|
evalCtx.setEvalReason(EnumEvalReason.EVALUATED_ALLOW_ACI);
|
evalCtx.setDecidingAci(allowAci);
|
return true;
|
}
|
}
|
}
|
// Nothing matched fall through.
|
evalCtx.setEvalReason(EnumEvalReason.NO_MATCHED_ALLOWS_ACIS);
|
evalCtx.setDecidingAci(null);
|
return false;
|
}
|
|
|
|
/**
|
* Test the attribute types of the search filter for access. This
|
* method supports the search right.
|
*
|
* @param container
|
* The container used in the access evaluation.
|
* @param filter
|
* The filter to check access on.
|
* @return True if all attribute types in the filter have access.
|
* @throws DirectoryException
|
* If there is a problem matching the entry using the
|
* provided filter.
|
*/
|
private boolean testFilter(AciLDAPOperationContainer container,
|
SearchFilter filter) throws DirectoryException
|
{
|
boolean ret = true;
|
// If the resource entry has a dn equal to "cn=debugsearch" and it
|
// contains the special attribute type "debugsearchindex", then the
|
// resource entry is a pseudo entry created for debug purposes.
|
// Return true if that is the case.
|
if (debugSearchIndexDN.equals(container.getResourceDN())
|
&& container.getResourceEntry().hasAttribute(debugSearchIndex))
|
{
|
return true;
|
}
|
switch (filter.getFilterType())
|
{
|
case AND:
|
case OR:
|
{
|
for (SearchFilter f : filter.getFilterComponents())
|
{
|
if (!testFilter(container, f))
|
{
|
return false;
|
}
|
}
|
break;
|
}
|
case NOT:
|
{
|
SearchFilter f = filter.getNotComponent();
|
ret = testFilter(container, f);
|
break;
|
}
|
default:
|
{
|
AttributeType attrType = filter.getAttributeType();
|
container.setCurrentAttributeType(attrType);
|
ret = accessAllowed(container);
|
}
|
}
|
return ret;
|
}
|
|
|
|
/**
|
* Evaluate an entry to be added to see if it has any "aci" attribute
|
* type. If it does, examines each "aci" attribute type value for
|
* syntax errors. All of the "aci" attribute type values must pass
|
* syntax check for the add operation to proceed. Any entry with an
|
* "aci" attribute type must have "modify-acl" privileges.
|
*
|
* @param entry
|
* The entry to be examined.
|
* @param operation
|
* The operation to to check privileges on.
|
* @param clientDN
|
* The authorization DN.
|
* @return True if the entry has no ACI attributes or if all of the
|
* "aci" attributes values pass ACI syntax checking.
|
* @throws DirectoryException
|
* If a modified ACI could not be decoded.
|
*/
|
private boolean verifySyntax(Entry entry, Operation operation,
|
DN clientDN) throws DirectoryException
|
{
|
if (entry.hasOperationalAttribute(aciType))
|
{
|
/*
|
* Check that the operation has "modify-acl" privileges since the
|
* entry to be added has an "aci" attribute type.
|
*/
|
if (!operation.getClientConnection().hasPrivilege(
|
Privilege.MODIFY_ACL, operation))
|
{
|
Message message =
|
INFO_ACI_ADD_FAILED_PRIVILEGE.get(String.valueOf(entry
|
.getDN()), String.valueOf(clientDN));
|
logError(message);
|
return false;
|
}
|
List<Attribute> attributeList =
|
entry.getOperationalAttribute(aciType, null);
|
for (Attribute attribute : attributeList)
|
{
|
for (AttributeValue value : attribute)
|
{
|
try
|
{
|
DN dn = entry.getDN();
|
Aci.decode(value.getValue(), dn);
|
}
|
catch (AciException ex)
|
{
|
Message message =
|
WARN_ACI_ADD_FAILED_DECODE.get(String.valueOf(entry
|
.getDN()), ex.getMessage());
|
throw new DirectoryException(
|
ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
|
}
|
}
|
}
|
}
|
return true;
|
}
|
}
|