/*
* 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 legal-notices/CDDLv1_0.txt
* or http://forgerock.org/license/CDDLv1.0.html.
* 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 legal-notices/CDDLv1_0.txt.
* 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-2014 ForgeRock AS
*/
package org.opends.server.workflowelement.localbackend;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.TreeMap;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.LocalizableMessageBuilder;
import org.forgerock.i18n.LocalizableMessageDescriptor;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.SearchScope;
import org.opends.server.api.AccessControlHandler;
import org.opends.server.api.Backend;
import org.opends.server.backends.RootDSEBackend;
import org.opends.server.controls.LDAPPostReadRequestControl;
import org.opends.server.controls.LDAPPostReadResponseControl;
import org.opends.server.controls.LDAPPreReadRequestControl;
import org.opends.server.controls.LDAPPreReadResponseControl;
import org.opends.server.core.*;
import org.opends.server.types.*;
import static org.opends.messages.CoreMessages.*;
/**
* This class defines a local backend workflow element; e-g an entity that
* handle the processing of an operation against a local backend.
*/
public class LocalBackendWorkflowElement
{
/**
* This class implements the workflow result code. The workflow result code
* contains an LDAP result code along with an LDAP error message.
*/
private static class SearchResultCode
{
/** The global result code. */
private ResultCode resultCode = ResultCode.UNDEFINED;
/** The global error message. */
private LocalizableMessageBuilder errorMessage = new LocalizableMessageBuilder(LocalizableMessage.EMPTY);
/**
* Creates a new instance of a workflow result code and initializes it with
* a result code and an error message.
*
* @param resultCode
* the initial value for the result code
* @param errorMessage
* the initial value for the error message
*/
SearchResultCode(ResultCode resultCode, LocalizableMessageBuilder errorMessage)
{
this.resultCode = resultCode;
this.errorMessage = errorMessage;
}
/**
* Elaborates a global result code. A workflow may execute an operation on
* several subordinate workflows. In such case, the parent workflow has to
* take into account all the subordinate result codes to elaborate a global
* result code. Sometimes, a referral result code has to be turned into a
* reference entry. When such case is occurring the
* elaborateGlobalResultCode method will return true. The global result code
* is elaborated as follows:
*
*
* -----------+------------+------------+-------------------------------
* new | current | resulting |
* resultCode | resultCode | resultCode | action
* -----------+------------+------------+-------------------------------
* SUCCESS NO_SUCH_OBJ SUCCESS -
* REFERRAL SUCCESS send reference entry to client
* other [unchanged] -
* ---------------------------------------------------------------------
* NO_SUCH_OBJ SUCCESS [unchanged] -
* REFERRAL [unchanged] -
* other [unchanged] -
* ---------------------------------------------------------------------
* REFERRAL SUCCESS [unchanged] send reference entry to client
* REFERRAL SUCCESS send reference entry to client
* NO_SUCH_OBJ REFERRAL -
* other [unchanged] send reference entry to client
* ---------------------------------------------------------------------
* others SUCCESS other -
* REFERRAL other send reference entry to client
* NO_SUCH_OBJ other -
* other2 [unchanged] -
* ---------------------------------------------------------------------
*
*
* @param newResultCode
* the new result code to take into account
* @param newErrorMessage
* the new error message associated to the new error code
* @return true if a referral result code must be turned into a
* reference entry
*/
private boolean elaborateGlobalResultCode(ResultCode newResultCode, LocalizableMessageBuilder newErrorMessage)
{
// if global result code has not been set yet then just take the new
// result code as is
if (resultCode == ResultCode.UNDEFINED)
{
resultCode = newResultCode;
errorMessage = new LocalizableMessageBuilder(newErrorMessage);
return false;
}
// Elaborate the new result code (see table in the description header).
switch (newResultCode.asEnum())
{
case SUCCESS:
switch (resultCode.asEnum())
{
case NO_SUCH_OBJECT:
resultCode = ResultCode.SUCCESS;
errorMessage = new LocalizableMessageBuilder(LocalizableMessage.EMPTY);
return false;
case REFERRAL:
resultCode = ResultCode.SUCCESS;
errorMessage = new LocalizableMessageBuilder(LocalizableMessage.EMPTY);
return true;
default:
// global resultCode remains the same
return false;
}
case NO_SUCH_OBJECT:
// global resultCode remains the same
return false;
case REFERRAL:
switch (resultCode.asEnum())
{
case REFERRAL:
resultCode = ResultCode.SUCCESS;
errorMessage = new LocalizableMessageBuilder(LocalizableMessage.EMPTY);
return true;
case NO_SUCH_OBJECT:
resultCode = ResultCode.REFERRAL;
errorMessage = new LocalizableMessageBuilder(LocalizableMessage.EMPTY);
return false;
default:
// global resultCode remains the same
return true;
}
default:
switch (resultCode.asEnum())
{
case REFERRAL:
resultCode = newResultCode;
errorMessage = new LocalizableMessageBuilder(newErrorMessage);
return true;
case SUCCESS:
case NO_SUCH_OBJECT:
resultCode = newResultCode;
errorMessage = new LocalizableMessageBuilder(newErrorMessage);
return false;
default:
// Do nothing (we don't want to override the first error)
return false;
}
}
}
}
private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
/** The backend's baseDN mapped by this object. */
private final DN baseDN;
/** the backend associated with the local workflow element. */
private final Backend> backend;
/** the set of local backend workflow elements registered with the server. */
private static TreeMap registeredLocalBackends =
new TreeMap();
/** A lock to guarantee safe concurrent access to the registeredLocalBackends variable. */
private static final Object registeredLocalBackendsLock = new Object();
/**
* Creates a new instance of the local backend workflow element.
*
* @param baseDN
* the backend's baseDN mapped by this object
* @param backend
* the backend associated to that workflow element
*/
public LocalBackendWorkflowElement(DN baseDN, Backend> backend)
{
this.baseDN = baseDN;
this.backend = backend;
}
/**
* Indicates whether the workflow element encapsulates a private local
* backend.
*
* @return true if the workflow element encapsulates a private
* local backend, false otherwise
*/
public boolean isPrivate()
{
return this.backend != null && this.backend.isPrivateBackend();
}
/**
* Creates and registers a local backend with the server.
*
* @param baseDN
* the backend's baseDN mapped by this object
* @param backend
* the backend to associate with the local backend workflow element
* @return the existing local backend workflow element if it was already
* created or a newly created local backend workflow element.
*/
public static LocalBackendWorkflowElement createAndRegister(DN baseDN, Backend> backend)
{
LocalBackendWorkflowElement localBackend = registeredLocalBackends.get(baseDN);
if (localBackend == null)
{
localBackend = new LocalBackendWorkflowElement(baseDN, backend);
registerLocalBackend(localBackend);
}
return localBackend;
}
/**
* Removes a local backend that was registered with the server.
*
* @param baseDN
* the identifier of the workflow to remove
*/
public static void remove(DN baseDN)
{
deregisterLocalBackend(baseDN);
}
/**
* Removes all the local backends that were registered with the server.
* This function is intended to be called when the server is shutting down.
*/
public static void removeAll()
{
synchronized (registeredLocalBackendsLock)
{
for (LocalBackendWorkflowElement localBackend : registeredLocalBackends.values())
{
deregisterLocalBackend(localBackend.getBaseDN());
}
}
}
/**
* Removes all the disallowed request controls from the provided operation.
*
* As per RFC 4511 4.1.11, if a disallowed request control is critical, then a
* DirectoryException is thrown with unavailableCriticalExtension. Otherwise,
* if the disallowed request control is non critical, it is removed because we
* do not want the backend to process it.
*
* @param targetDN
* the target DN on which the operation applies
* @param op
* the operation currently processed
* @throws DirectoryException
* If a disallowed request control is critical, thrown with
* unavailableCriticalExtension. If an error occurred while
* performing the access control check. For example, if an attribute
* could not be decoded. Care must be taken not to expose any
* potentially sensitive information in the exception.
*/
static void removeAllDisallowedControls(DN targetDN, Operation op)
throws DirectoryException
{
final List requestControls = op.getRequestControls();
if (requestControls != null && !requestControls.isEmpty())
{
for (Iterator iter = requestControls.iterator(); iter.hasNext();)
{
final Control control = iter.next();
if (!getAccessControlHandler().isAllowed(targetDN, op, control))
{
// As per RFC 4511 4.1.11.
if (control.isCritical())
{
throw new DirectoryException(
ResultCode.UNAVAILABLE_CRITICAL_EXTENSION,
ERR_CONTROL_INSUFFICIENT_ACCESS_RIGHTS.get(control.getOID()));
}
// We do not want the backend to process this non-critical control, so
// remove it.
iter.remove();
}
}
}
}
/**
* Returns a new {@link DirectoryException} built from the provided
* resultCodes and messages. Depending on whether ACIs prevent information
* disclosure, the provided resultCode and message will be masked and
* altResultCode and altMessage will be used instead.
*
* @param operation
* the operation for which to check if ACIs prevent information
* disclosure
* @param entry
* the entry for which to check if ACIs prevent information
* disclosure, if null, then a fake entry will be created from the
* entryDN parameter
* @param entryDN
* the entry dn for which to check if ACIs prevent information
* disclosure. Only used if entry is null.
* @param resultCode
* the result code to put on the DirectoryException if ACIs allow
* disclosure. Otherwise it will be put on the DirectoryException as
* a masked result code.
* @param message
* the message to put on the DirectoryException if ACIs allow
* disclosure. Otherwise it will be put on the DirectoryException as
* a masked message.
* @param altResultCode
* the result code to put on the DirectoryException if ACIs do not
* allow disclosing the resultCode.
* @param altMessage
* the result code to put on the DirectoryException if ACIs do not
* allow disclosing the message.
* @return a new DirectoryException containing the provided resultCodes and
* messages depending on ACI allowing disclosure or not
* @throws DirectoryException
* If an error occurred while performing the access control check.
*/
static DirectoryException newDirectoryException(Operation operation,
Entry entry, DN entryDN, ResultCode resultCode, LocalizableMessage message,
ResultCode altResultCode, LocalizableMessage altMessage) throws DirectoryException
{
if (getAccessControlHandler().canDiscloseInformation(entry, entryDN, operation))
{
return new DirectoryException(resultCode, message);
}
// replacement reason returned to the user
final DirectoryException ex = new DirectoryException(altResultCode, altMessage);
// real underlying reason
ex.setMaskedResultCode(resultCode);
ex.setMaskedMessage(message);
return ex;
}
/**
* Sets the provided resultCodes and messages on the provided operation.
* Depending on whether ACIs prevent information disclosure, the provided
* resultCode and message will be masked and altResultCode and altMessage will
* be used instead.
*
* @param operation
* the operation for which to check if ACIs prevent information
* disclosure
* @param entry
* the entry for which to check if ACIs prevent information
* disclosure, if null, then a fake entry will be created from the
* entryDN parameter
* @param entryDN
* the entry dn for which to check if ACIs prevent information
* disclosure. Only used if entry is null.
* @param resultCode
* the result code to put on the DirectoryException if ACIs allow
* disclosure. Otherwise it will be put on the DirectoryException as
* a masked result code.
* @param message
* the message to put on the DirectoryException if ACIs allow
* disclosure. Otherwise it will be put on the DirectoryException as
* a masked message.
* @param altResultCode
* the result code to put on the DirectoryException if ACIs do not
* allow disclosing the resultCode.
* @param altMessage
* the result code to put on the DirectoryException if ACIs do not
* allow disclosing the message.
* @throws DirectoryException
* If an error occurred while performing the access control check.
*/
static void setResultCodeAndMessageNoInfoDisclosure(Operation operation,
Entry entry, DN entryDN, ResultCode resultCode, LocalizableMessage message,
ResultCode altResultCode, LocalizableMessage altMessage) throws DirectoryException
{
if (getAccessControlHandler().canDiscloseInformation(entry, entryDN, operation))
{
operation.setResultCode(resultCode);
operation.appendErrorMessage(message);
}
else
{
// replacement reason returned to the user
operation.setResultCode(altResultCode);
operation.appendErrorMessage(altMessage);
// real underlying reason
operation.setMaskedResultCode(resultCode);
operation.appendMaskedErrorMessage(message);
}
}
/**
* Removes the matchedDN from the supplied operation if ACIs prevent its
* disclosure.
*
* @param operation
* where to filter the matchedDN from
*/
static void filterNonDisclosableMatchedDN(Operation operation)
{
if (operation.getMatchedDN() == null)
{
return;
}
try
{
if (!getAccessControlHandler().canDiscloseInformation(null, operation.getMatchedDN(), operation))
{
operation.setMatchedDN(null);
}
}
catch (DirectoryException de)
{
logger.traceException(de);
operation.setResponseData(de);
// At this point it is impossible to tell whether the matchedDN can be
// disclosed. It is probably safer to hide it by default.
operation.setMatchedDN(null);
}
}
/**
* Adds the post-read response control to the response if requested.
*
* @param operation
* The update operation.
* @param postReadRequest
* The request control, if present.
* @param entry
* The post-update entry.
*/
static void addPostReadResponse(final Operation operation,
final LDAPPostReadRequestControl postReadRequest, final Entry entry)
{
if (postReadRequest == null)
{
return;
}
/*
* Virtual and collective attributes are only added to an entry when it is
* read from the backend, not before it is written, so we need to add them
* ourself.
*/
final Entry fullEntry = entry.duplicate(true);
// Even though the associated update succeeded,
// we should still check whether or not we should return the entry.
final SearchResultEntry unfilteredSearchEntry = new SearchResultEntry(fullEntry, null);
if (getAccessControlHandler().maySend(operation, unfilteredSearchEntry))
{
// Filter the entry based on the control's attribute list.
final Entry filteredEntry = fullEntry.filterEntry(postReadRequest.getRequestedAttributes(), false, false, false);
final SearchResultEntry filteredSearchEntry = new SearchResultEntry(filteredEntry, null);
// Strip out any attributes which access control denies access to.
getAccessControlHandler().filterEntry(operation, unfilteredSearchEntry, filteredSearchEntry);
operation.addResponseControl(new LDAPPostReadResponseControl(filteredSearchEntry));
}
}
/**
* Adds the pre-read response control to the response if requested.
*
* @param operation
* The update operation.
* @param preReadRequest
* The request control, if present.
* @param entry
* The pre-update entry.
*/
static void addPreReadResponse(final Operation operation,
final LDAPPreReadRequestControl preReadRequest, final Entry entry)
{
if (preReadRequest == null)
{
return;
}
// Even though the associated update succeeded,
// we should still check whether or not we should return the entry.
final SearchResultEntry unfilteredSearchEntry = new SearchResultEntry(entry, null);
if (getAccessControlHandler().maySend(operation, unfilteredSearchEntry))
{
// Filter the entry based on the control's attribute list.
final Entry filteredEntry = entry.filterEntry(preReadRequest.getRequestedAttributes(), false, false, false);
final SearchResultEntry filteredSearchEntry = new SearchResultEntry(filteredEntry, null);
// Strip out any attributes which access control denies access to.
getAccessControlHandler().filterEntry(operation, unfilteredSearchEntry, filteredSearchEntry);
operation.addResponseControl(new LDAPPreReadResponseControl(filteredSearchEntry));
}
}
private static AccessControlHandler> getAccessControlHandler()
{
return AccessControlConfigManager.getInstance().getAccessControlHandler();
}
/**
* Registers a local backend with the server.
*
* @param localBackend the local backend to register with the server
*/
private static void registerLocalBackend(LocalBackendWorkflowElement localBackend)
{
synchronized (registeredLocalBackendsLock)
{
DN baseDN = localBackend.getBaseDN();
LocalBackendWorkflowElement existingLocalBackend = registeredLocalBackends.get(baseDN);
if (existingLocalBackend == null)
{
TreeMap newLocalBackends =
new TreeMap(registeredLocalBackends);
newLocalBackends.put(baseDN, localBackend);
registeredLocalBackends = newLocalBackends;
}
}
}
/**
* Deregisters a local backend with the server.
*
* @param baseDN
* the identifier of the local backend to remove
*/
private static void deregisterLocalBackend(DN baseDN)
{
synchronized (registeredLocalBackendsLock)
{
LocalBackendWorkflowElement existingLocalBackend = registeredLocalBackends.get(baseDN);
if (existingLocalBackend != null)
{
TreeMap newLocalBackends =
new TreeMap(registeredLocalBackends);
newLocalBackends.remove(baseDN);
registeredLocalBackends = newLocalBackends;
}
}
}
/**
* Executes the workflow for an operation.
*
* @param operation
* the operation to execute
* @throws CanceledOperationException
* if this operation should be canceled
*/
public void execute(Operation operation) throws CanceledOperationException {
switch (operation.getOperationType())
{
case BIND:
new LocalBackendBindOperation((BindOperation) operation).processLocalBind(this);
break;
case SEARCH:
new LocalBackendSearchOperation((SearchOperation) operation).processLocalSearch(this);
break;
case ADD:
new LocalBackendAddOperation((AddOperation) operation).processLocalAdd(this);
break;
case DELETE:
new LocalBackendDeleteOperation((DeleteOperation) operation).processLocalDelete(this);
break;
case MODIFY:
new LocalBackendModifyOperation((ModifyOperation) operation).processLocalModify(this);
break;
case MODIFY_DN:
new LocalBackendModifyDNOperation((ModifyDNOperation) operation).processLocalModifyDN(this);
break;
case COMPARE:
new LocalBackendCompareOperation((CompareOperation) operation).processLocalCompare(this);
break;
case ABANDON:
// There is no processing for an abandon operation.
break;
default:
throw new AssertionError("Attempted to execute an invalid operation " +
"type: " + operation.getOperationType() +
" (" + operation + ")");
}
}
/**
* Attaches the current local operation to the global operation so that
* operation runner can execute local operation post response later on.
*
* @param subtype of Operation
* @param subtype of LocalBackendOperation
* @param globalOperation the global operation to which local operation
* should be attached to
* @param currentLocalOperation the local operation to attach to the global
* operation
*/
@SuppressWarnings("unchecked")
static void attachLocalOperation(O globalOperation, L currentLocalOperation)
{
List> existingAttachment = (List>) globalOperation.getAttachment(Operation.LOCALBACKENDOPERATIONS);
List newAttachment = new ArrayList();
if (existingAttachment != null)
{
// This line raises an unchecked conversion warning.
// There is nothing we can do to prevent this warning
// so let's get rid of it since we know the cast is safe.
newAttachment.addAll ((List) existingAttachment);
}
newAttachment.add (currentLocalOperation);
globalOperation.setAttachment(Operation.LOCALBACKENDOPERATIONS, newAttachment);
}
/**
* Provides the workflow element identifier.
*
* @return the workflow element identifier
*/
public DN getBaseDN()
{
return baseDN;
}
/**
* Gets the backend associated with this local backend workflow
* element.
*
* @return The backend associated with this local backend workflow
* element.
*/
public Backend> getBackend()
{
return backend;
}
/**
* Checks if an update operation can be performed against a backend. The
* operation will be rejected based on the server and backend writability
* modes.
*
* @param backend
* The backend handling the update.
* @param op
* The update operation.
* @param entryDN
* The name of the entry being updated.
* @param serverMsg
* The message to log if the update was rejected because the server
* is read-only.
* @param backendMsg
* The message to log if the update was rejected because the backend
* is read-only.
* @throws DirectoryException
* If the update operation has been rejected.
*/
static void checkIfBackendIsWritable(Backend> backend, Operation op,
DN entryDN, LocalizableMessageDescriptor.Arg1