/* * 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 TreeMaptrue 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 Listtrue if the control has been processed, false if not
* @throws DirectoryException
*/
static boolean 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 false;
}
checkAciForProxyAuthControl(operation, authorizationEntry);
operation.setAuthorizationEntry(authorizationEntry);
if (authorizationEntry == null)
{
operation.setProxiedAuthorizationDN(DN.NULL_DN);
}
else
{
operation.setProxiedAuthorizationDN(authorizationEntry.getName());
}
return true;
}
/**
* 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