/*
* 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-2015 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.controls.ProxiedAuthV1Control;
import org.opends.server.controls.ProxiedAuthV2Control;
import org.opends.server.core.*;
import org.opends.server.types.*;
import static org.opends.messages.CoreMessages.*;
import static org.opends.messages.ProtocolMessages.ERR_PROXYAUTH_AUTHZ_NOT_PERMITTED;
import static org.opends.server.util.ServerConstants.*;
/**
* 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
*/
private 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());
}
}
}
/**
* Check if an OID is for a proxy authorization control.
*
* @param oid The OID to check
* @return true if the OID is for a proxy auth v1 or v2 control,
* false otherwise.
*/
static boolean isProxyAuthzControl(String oid)
{
return OID_PROXIED_AUTH_V1.equals(oid) || OID_PROXIED_AUTH_V2.equals(oid);
}
/**
* 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 operation
* 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 operation) throws DirectoryException
{
List requestControls = operation.getRequestControls();
if (requestControls != null && !requestControls.isEmpty())
{
for (Iterator iter = requestControls.iterator(); iter.hasNext();)
{
final Control control = iter.next();
if (isProxyAuthzControl(control.getOID()))
{
continue;
}
if (!getAccessControlHandler().isAllowed(targetDN, operation, 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();
}
}
}
}
/**
* Evaluate all aci and privilege checks for any proxy auth controls.
* This must be done before evaluating all other controls so that their aci
* can then be checked correctly.
*
* @param operation The operation containing the controls
* @throws DirectoryException if a proxy auth control is found but cannot
* be used.
*/
static void evaluateProxyAuthControls(Operation operation) throws DirectoryException
{
final List requestControls = operation.getRequestControls();
if (requestControls != null && !requestControls.isEmpty())
{
for (Control control : requestControls)
{
final String oid = control.getOID();
if (isProxyAuthzControl(oid))
{
if (getAccessControlHandler().isAllowed(operation.getClientConnection()
.getAuthenticationInfo().getAuthenticationDN(), operation, control))
{
processProxyAuthControls(operation, oid);
}
else
{
// 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()));
}
}
}
}
}
}
/**
* Check the requester has the PROXIED_AUTH privilege in order to be able to use a proxy auth control.
*
* @param operation The operation being checked
* @throws DirectoryException If insufficient privileges are detected
*/
private static void checkPrivilegeForProxyAuthControl(Operation operation) throws DirectoryException
{
if (! operation.getClientConnection().hasPrivilege(Privilege.PROXIED_AUTH, operation))
{
throw new DirectoryException(ResultCode.AUTHORIZATION_DENIED,
ERR_PROXYAUTH_INSUFFICIENT_PRIVILEGES.get());
}
}
/**
* Check the requester has the authorization user in scope of proxy aci.
*
* @param operation The operation being checked
* @param authorizationEntry The entry being authorized as (e.g. from a proxy auth control)
* @throws DirectoryException If no proxy permission is allowed
*/
private static void checkAciForProxyAuthControl(Operation operation, Entry authorizationEntry)
throws DirectoryException
{
if (! AccessControlConfigManager.getInstance().getAccessControlHandler()
.mayProxy(operation.getClientConnection().getAuthenticationInfo().getAuthenticationEntry(),
authorizationEntry, operation))
{
throw new DirectoryException(ResultCode.AUTHORIZATION_DENIED,
ERR_PROXYAUTH_AUTHZ_NOT_PERMITTED.get(authorizationEntry.getName()));
}
}
/**
* Process the operation control with the given oid if it is a proxy auth control.
*
* Privilege and initial aci checks on the authenticating user are performed. The authenticating
* user must have the proxied-auth privilege, and the authz user must be in the scope of aci
* allowing the proxy right to the authenticating user.
*
* @param operation The operation containing the control(s)
* @param oid The OID of the detected proxy auth control
* @throws DirectoryException
*/
private static void processProxyAuthControls(Operation operation, String oid)
throws DirectoryException
{
final Entry authorizationEntry;
if (OID_PROXIED_AUTH_V1.equals(oid))
{
final ProxiedAuthV1Control proxyControlV1 = operation.getRequestControl(ProxiedAuthV1Control.DECODER);
// Log usage of legacy proxy authz V1 control.
operation.addAdditionalLogItem(AdditionalLogItem.keyOnly(operation.getClass(),
"obsoleteProxiedAuthzV1Control"));
checkPrivilegeForProxyAuthControl(operation);
authorizationEntry = proxyControlV1.getAuthorizationEntry();
}
else if (OID_PROXIED_AUTH_V2.equals(oid))
{
final ProxiedAuthV2Control proxyControlV2 = operation.getRequestControl(ProxiedAuthV2Control.DECODER);
checkPrivilegeForProxyAuthControl(operation);
authorizationEntry = proxyControlV2.getAuthorizationEntry();
}
else
{
return;
}
checkAciForProxyAuthControl(operation, authorizationEntry);
operation.setAuthorizationEntry(authorizationEntry);
operation.setProxiedAuthorizationDN(
authorizationEntry != null ? authorizationEntry.getName() : DN.NULL_DN);
}
/**
* 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
*/
private 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