/* * The contents of this file are subject to the terms of the Common Development and * Distribution License (the License). You may not use this file except in compliance with the * License. * * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the * specific language governing permission and limitations under the License. * * When distributing Covered Software, include this CDDL Header Notice in each file and include * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions Copyright [year] [name of copyright owner]". * * Copyright 2008-2010 Sun Microsystems, Inc. * Portions Copyright 2011-2016 ForgeRock AS. */ package org.opends.server.workflowelement.localbackend; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import java.util.concurrent.atomic.AtomicBoolean; import org.forgerock.i18n.LocalizableMessage; import org.forgerock.i18n.LocalizableMessageBuilder; import org.forgerock.i18n.slf4j.LocalizedLogger; import org.forgerock.opendj.ldap.AVA; import org.forgerock.opendj.ldap.ByteString; import org.forgerock.opendj.ldap.ModificationType; import org.forgerock.opendj.ldap.ResultCode; import org.forgerock.opendj.ldap.schema.AttributeType; import org.opends.server.api.AccessControlHandler; import org.opends.server.api.Backend; import org.opends.server.api.ClientConnection; import org.opends.server.api.SynchronizationProvider; import org.opends.server.controls.LDAPAssertionRequestControl; import org.opends.server.controls.LDAPPostReadRequestControl; import org.opends.server.controls.LDAPPreReadRequestControl; import org.opends.server.core.AccessControlConfigManager; import org.opends.server.core.DirectoryServer; import org.opends.server.core.ModifyDNOperation; import org.opends.server.core.ModifyDNOperationWrapper; import org.opends.server.core.PersistentSearch; import org.opends.server.types.Attribute; import org.opends.server.types.Attributes; import org.opends.server.types.CanceledOperationException; import org.opends.server.types.Control; import org.forgerock.opendj.ldap.DN; import org.opends.server.types.DirectoryException; import org.opends.server.types.Entry; import org.opends.server.types.LockManager.DNLock; import org.opends.server.types.Modification; import org.forgerock.opendj.ldap.RDN; import org.opends.server.types.SearchFilter; import org.opends.server.types.operation.PostOperationModifyDNOperation; import org.opends.server.types.operation.PostResponseModifyDNOperation; import org.opends.server.types.operation.PostSynchronizationModifyDNOperation; import org.opends.server.types.operation.PreOperationModifyDNOperation; import static org.opends.messages.CoreMessages.*; import static org.opends.server.core.DirectoryServer.*; import static org.opends.server.types.AbstractOperation.*; import static org.opends.server.util.ServerConstants.*; import static org.opends.server.util.StaticUtils.*; import static org.opends.server.workflowelement.localbackend.LocalBackendWorkflowElement.*; /** * This class defines an operation used to move an entry in a local backend * of the Directory Server. */ public class LocalBackendModifyDNOperation extends ModifyDNOperationWrapper implements PreOperationModifyDNOperation, PostOperationModifyDNOperation, PostResponseModifyDNOperation, PostSynchronizationModifyDNOperation { private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); /** The backend in which the operation is to be processed. */ private Backend backend; /** Indicates whether the no-op control was included in the request. */ private boolean noOp; /** The client connection on which this operation was requested. */ private ClientConnection clientConnection; /** The original DN of the entry. */ private DN entryDN; /** The current entry, before it is renamed. */ private Entry currentEntry; /** The new entry, as it will appear after it has been renamed. */ private Entry newEntry; /** The LDAP post-read request control, if present in the request. */ private LDAPPostReadRequestControl postReadRequest; /** The LDAP pre-read request control, if present in the request. */ private LDAPPreReadRequestControl preReadRequest; /** The new RDN for the entry. */ private RDN newRDN; /** * Creates a new operation that may be used to move an entry in a * local backend of the Directory Server. * * @param operation The operation to enhance. */ public LocalBackendModifyDNOperation (ModifyDNOperation operation) { super(operation); LocalBackendWorkflowElement.attachLocalOperation (operation, this); } /** * Retrieves the current entry, before it is renamed. This will not be * available to pre-parse plugins or during the conflict resolution portion of * the synchronization processing. * * @return The current entry, or null if it is not yet * available. */ @Override public final Entry getOriginalEntry() { return currentEntry; } /** * Retrieves the new entry, as it will appear after it is renamed. This will * not be available to pre-parse plugins or during the conflict resolution * portion of the synchronization processing. * * @return The updated entry, or null if it is not yet * available. */ @Override public final Entry getUpdatedEntry() { return newEntry; } /** * Process this modify DN operation in a local backend. * * @param wfe * The local backend work-flow element. * @throws CanceledOperationException * if this operation should be cancelled */ public void processLocalModifyDN(final LocalBackendWorkflowElement wfe) throws CanceledOperationException { this.backend = wfe.getBackend(); clientConnection = getClientConnection(); // Check for a request to cancel this operation. checkIfCanceled(false); try { AtomicBoolean executePostOpPlugins = new AtomicBoolean(false); processModifyDN(executePostOpPlugins); // Invoke the post-operation or post-synchronization modify DN plugins. if (isSynchronizationOperation()) { if (getResultCode() == ResultCode.SUCCESS) { getPluginConfigManager().invokePostSynchronizationModifyDNPlugins(this); } } else if (executePostOpPlugins.get()) { if (!processOperationResult(this, getPluginConfigManager().invokePostOperationModifyDNPlugins(this))) { return; } } } finally { LocalBackendWorkflowElement.filterNonDisclosableMatchedDN(this); } // Register a post-response call-back which will notify persistent // searches and change listeners. if (getResultCode() == ResultCode.SUCCESS) { registerPostResponseCallback(new Runnable() { @Override public void run() { for (PersistentSearch psearch : backend.getPersistentSearches()) { psearch.processModifyDN(newEntry, currentEntry.getName()); } } }); } } private void processModifyDN(AtomicBoolean executePostOpPlugins) throws CanceledOperationException { // Process the entry DN, newRDN, and newSuperior elements from their raw // forms as provided by the client to the forms required for the rest of // the modify DN processing. entryDN = getEntryDN(); newRDN = getNewRDN(); if (newRDN == null) { return; } DN newSuperior = getNewSuperior(); if (newSuperior == null && getRawNewSuperior() != null) { return; } // Construct the new DN to use for the entry. DN parentDN; if (newSuperior == null) { parentDN = DirectoryServer.getParentDNInSuffix(entryDN); } else { if (newSuperior.isSubordinateOrEqualTo(entryDN)) { setResultCode(ResultCode.UNWILLING_TO_PERFORM); appendErrorMessage(ERR_MODDN_NEW_SUPERIOR_IN_SUBTREE.get(entryDN, newSuperior)); return; } parentDN = newSuperior; } if (parentDN == null || parentDN.isRootDN()) { setResultCode(ResultCode.UNWILLING_TO_PERFORM); appendErrorMessage(ERR_MODDN_NO_PARENT.get(entryDN)); return; } DN newDN = parentDN.child(newRDN); // Get the backend for the current entry, and the backend for the new // entry. If either is null, or if they are different, then fail. Backend currentBackend = backend; if (currentBackend == null) { setResultCode(ResultCode.NO_SUCH_OBJECT); appendErrorMessage(ERR_MODDN_NO_BACKEND_FOR_CURRENT_ENTRY.get(entryDN)); return; } Backend newBackend = DirectoryServer.getBackend(newDN); if (newBackend == null) { setResultCode(ResultCode.NO_SUCH_OBJECT); appendErrorMessage(ERR_MODDN_NO_BACKEND_FOR_NEW_ENTRY.get(entryDN, newDN)); return; } else if (!currentBackend.equals(newBackend)) { setResultCode(ResultCode.UNWILLING_TO_PERFORM); appendErrorMessage(ERR_MODDN_DIFFERENT_BACKENDS.get(entryDN, newDN)); return; } // Check for a request to cancel this operation. checkIfCanceled(false); /* * Acquire subtree write locks for the current and new DN. Be careful to avoid deadlocks by * taking the locks in a well defined order. */ DNLock currentLock = null; DNLock newLock = null; try { if (entryDN.compareTo(newDN) < 0) { currentLock = DirectoryServer.getLockManager().tryWriteLockSubtree(entryDN); newLock = DirectoryServer.getLockManager().tryWriteLockSubtree(newDN); } else { newLock = DirectoryServer.getLockManager().tryWriteLockSubtree(newDN); currentLock = DirectoryServer.getLockManager().tryWriteLockSubtree(entryDN); } if (currentLock == null) { setResultCode(ResultCode.BUSY); appendErrorMessage(ERR_MODDN_CANNOT_LOCK_CURRENT_DN.get(entryDN)); return; } if (newLock == null) { setResultCode(ResultCode.BUSY); appendErrorMessage(ERR_MODDN_CANNOT_LOCK_NEW_DN.get(entryDN, newDN)); return; } // Check for a request to cancel this operation. checkIfCanceled(false); // Get the current entry from the appropriate backend. If it doesn't // exist, then fail. currentEntry = currentBackend.getEntry(entryDN); if (getOriginalEntry() == null) { // See if one of the entry's ancestors exists. setMatchedDN(findMatchedDN(entryDN)); setResultCode(ResultCode.NO_SUCH_OBJECT); appendErrorMessage(ERR_MODDN_NO_CURRENT_ENTRY.get(entryDN)); return; } // Check to see if there are any controls in the request. If so, then // see if there is any special processing required. handleRequestControls(); // Check to see if the client has permission to perform the // modify DN. // FIXME: for now assume that this will check all permission // pertinent to the operation. This includes proxy authorization // and any other controls specified. // FIXME: earlier checks to see if the entry or new superior // already exists may have already exposed sensitive information // to the client. try { if (!getAccessControlHandler().isAllowed(this)) { setResultCodeAndMessageNoInfoDisclosure(currentEntry, entryDN, ResultCode.INSUFFICIENT_ACCESS_RIGHTS, ERR_MODDN_AUTHZ_INSUFFICIENT_ACCESS_RIGHTS.get(entryDN)); return; } } catch (DirectoryException e) { setResultCode(e.getResultCode()); appendErrorMessage(e.getMessageObject()); return; } // Duplicate the entry and set its new DN. Also, create an empty list // to hold the attribute-level modifications. newEntry = currentEntry.duplicate(false); newEntry.setDN(newDN); // init the modifications addModification(null); List modifications = getModifications(); if (!handleConflictResolution()) { return; } // Apply any changes to the entry based on the change in its RDN. // Also perform schema checking on the updated entry. applyRDNChanges(modifications); // If the operation is not a synchronization operation, // - Apply the RDN changes. // - Invoke the pre-operation modify DN plugins. // - apply additional modifications provided by the plugins. // If the operation is a synchronization operation // - apply the operation as it was originally done on the master. if (!isSynchronizationOperation()) { // Check for a request to cancel this operation. checkIfCanceled(false); // Get a count of the current number of modifications. The // pre-operation plugins may alter this list, and we need to be able // to identify which changes were made after they're done. int modCount = modifications.size(); executePostOpPlugins.set(true); if (!processOperationResult(this, getPluginConfigManager().invokePreOperationModifyDNPlugins(this))) { return; } // Check to see if any of the pre-operation plugins made any changes // to the entry. If so, then apply them. if (modifications.size() > modCount) { applyPreOpModifications(modifications, modCount, true); } } else { applyPreOpModifications(modifications, 0, false); } LocalBackendWorkflowElement.checkIfBackendIsWritable(currentBackend, this, entryDN, ERR_MODDN_SERVER_READONLY, ERR_MODDN_BACKEND_READONLY); if (noOp) { appendErrorMessage(INFO_MODDN_NOOP.get()); setResultCode(ResultCode.NO_OPERATION); } else { if (!processPreOperation()) { return; } currentBackend.renameEntry(entryDN, newEntry, this); } // Attach the pre-read and/or post-read controls to the response if // appropriate. LocalBackendWorkflowElement.addPreReadResponse(this, preReadRequest, currentEntry); LocalBackendWorkflowElement.addPostReadResponse(this, postReadRequest, newEntry); if (!noOp) { setResultCode(ResultCode.SUCCESS); } } catch (DirectoryException de) { logger.traceException(de); setResponseData(de); return; } finally { if (currentLock != null) { currentLock.unlock(); } if (newLock != null) { newLock.unlock(); } processSynchPostOperationPlugins(); } } private DirectoryException newDirectoryException(Entry entry, ResultCode resultCode, LocalizableMessage message) throws DirectoryException { return LocalBackendWorkflowElement.newDirectoryException(this, entry, null, resultCode, message, ResultCode.NO_SUCH_OBJECT, ERR_MODDN_NO_CURRENT_ENTRY.get(entryDN)); } private void setResultCodeAndMessageNoInfoDisclosure(Entry entry, DN entryDN, ResultCode realResultCode, LocalizableMessage realMessage) throws DirectoryException { LocalBackendWorkflowElement.setResultCodeAndMessageNoInfoDisclosure(this, entry, entryDN, realResultCode, realMessage, ResultCode.NO_SUCH_OBJECT, ERR_MODDN_NO_CURRENT_ENTRY.get(entryDN)); } /** * Processes the set of controls included in the request. * * @throws DirectoryException If a problem occurs that should cause the * modify DN operation to fail. */ private void handleRequestControls() throws DirectoryException { LocalBackendWorkflowElement.evaluateProxyAuthControls(this); LocalBackendWorkflowElement.removeAllDisallowedControls(entryDN, this); for (ListIterator iter = getRequestControls().listIterator(); iter.hasNext();) { final Control c = iter.next(); final String oid = c.getOID(); if (OID_LDAP_ASSERTION.equals(oid)) { LDAPAssertionRequestControl assertControl = getRequestControl(LDAPAssertionRequestControl.DECODER); SearchFilter filter; try { filter = assertControl.getSearchFilter(); } catch (DirectoryException de) { logger.traceException(de); throw newDirectoryException(currentEntry, de.getResultCode(), ERR_MODDN_CANNOT_PROCESS_ASSERTION_FILTER.get(entryDN, de.getMessageObject())); } // Check if the current user has permission to make this determination. if (!getAccessControlHandler().isAllowed(this, currentEntry, filter)) { throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS, ERR_CONTROL_INSUFFICIENT_ACCESS_RIGHTS.get(oid)); } try { if (!filter.matchesEntry(currentEntry)) { throw newDirectoryException(currentEntry, ResultCode.ASSERTION_FAILED, ERR_MODDN_ASSERTION_FAILED.get(entryDN)); } } catch (DirectoryException de) { if (de.getResultCode() == ResultCode.ASSERTION_FAILED) { throw de; } logger.traceException(de); throw newDirectoryException(currentEntry, de.getResultCode(), ERR_MODDN_CANNOT_PROCESS_ASSERTION_FILTER.get(entryDN, de.getMessageObject())); } } else if (OID_LDAP_NOOP_OPENLDAP_ASSIGNED.equals(oid)) { noOp = true; } else if (OID_LDAP_READENTRY_PREREAD.equals(oid)) { preReadRequest = getRequestControl(LDAPPreReadRequestControl.DECODER); iter.set(preReadRequest); } else if (OID_LDAP_READENTRY_POSTREAD.equals(oid)) { if (c instanceof LDAPPostReadRequestControl) { postReadRequest = (LDAPPostReadRequestControl) c; } else { postReadRequest = getRequestControl(LDAPPostReadRequestControl.DECODER); iter.set(postReadRequest); } } else if (LocalBackendWorkflowElement.isProxyAuthzControl(oid)) { continue; } else if (c.isCritical() && !backend.supportsControl(oid)) { throw new DirectoryException(ResultCode.UNAVAILABLE_CRITICAL_EXTENSION, ERR_MODDN_UNSUPPORTED_CRITICAL_CONTROL.get(entryDN, oid)); } } } private AccessControlHandler getAccessControlHandler() { return AccessControlConfigManager.getInstance().getAccessControlHandler(); } /** * Updates the entry so that its attributes are changed to reflect the changes * to the RDN. This also performs schema checking on the updated entry. * * @param modifications A list to hold the modifications made to the entry. * * @throws DirectoryException If a problem occurs that should cause the * modify DN operation to fail. */ private void applyRDNChanges(List modifications) throws DirectoryException { // If we should delete the old RDN values from the entry, then do so. if (deleteOldRDN()) { for (AVA ava : entryDN.rdn()) { Attribute a = Attributes.create( ava.getAttributeType(), ava.getAttributeName(), ava.getAttributeValue()); // If the associated attribute type is marked NO-USER-MODIFICATION, then // refuse the update. if (a.getAttributeDescription().getAttributeType().isNoUserModification() && !isInternalOperation() && !isSynchronizationOperation()) { throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, ERR_MODDN_OLD_RDN_ATTR_IS_NO_USER_MOD.get(entryDN, a.getAttributeDescription())); } List missingValues = new LinkedList<>(); newEntry.removeAttribute(a, missingValues); if (missingValues.isEmpty()) { modifications.add(new Modification(ModificationType.DELETE, a)); } } } // Add the new RDN values to the entry. for (AVA ava : newRDN) { Attribute a = Attributes.create( ava.getAttributeType(), ava.getAttributeName(), ava.getAttributeValue()); List duplicateValues = new LinkedList<>(); newEntry.addAttribute(a, duplicateValues); if (duplicateValues.isEmpty()) { // If the associated attribute type is marked NO-USER-MODIFICATION, then // refuse the update. if (a.getAttributeDescription().getAttributeType().isNoUserModification()) { if (!isInternalOperation() && !isSynchronizationOperation()) { throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, ERR_MODDN_NEW_RDN_ATTR_IS_NO_USER_MOD.get(entryDN, a.getAttributeDescription())); } } else { modifications.add(new Modification(ModificationType.ADD, a)); } } } // If the server is configured to check the schema and the operation is not // a synchronization operation, make sure that the resulting entry is valid // as per the server schema. if (DirectoryServer.checkSchema() && !isSynchronizationOperation()) { LocalizableMessageBuilder invalidReason = new LocalizableMessageBuilder(); if (! newEntry.conformsToSchema(null, false, true, true, invalidReason)) { throw new DirectoryException(ResultCode.OBJECTCLASS_VIOLATION, ERR_MODDN_VIOLATES_SCHEMA.get(entryDN, invalidReason)); } for (AVA ava : newRDN) { AttributeType at = ava.getAttributeType(); if (at.isObsolete()) { throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, ERR_MODDN_NEWRDN_ATTR_IS_OBSOLETE.get(entryDN, at.getNameOrOID())); } } } } /** * Applies any modifications performed during pre-operation plugin processing. * This also performs schema checking for the updated entry. * * @param modifications A list containing the modifications made to the * entry. * @param startPos The position in the list at which the pre-operation * modifications start. * @param checkSchema A boolean allowing to control if schema must be * checked * * @throws DirectoryException If a problem occurs that should cause the * modify DN operation to fail. */ private void applyPreOpModifications(List modifications, int startPos, boolean checkSchema) throws DirectoryException { for (int i=startPos; i < modifications.size(); i++) { Modification m = modifications.get(i); Attribute a = m.getAttribute(); switch (m.getModificationType().asEnum()) { case ADD: List duplicateValues = new LinkedList<>(); newEntry.addAttribute(a, duplicateValues); break; case DELETE: List missingValues = new LinkedList<>(); newEntry.removeAttribute(a, missingValues); break; case REPLACE: newEntry.replaceAttribute(a); break; case INCREMENT: newEntry.incrementAttribute(a); break; } } // Make sure that the updated entry still conforms to the server // schema. if (DirectoryServer.checkSchema() && checkSchema) { LocalizableMessageBuilder invalidReason = new LocalizableMessageBuilder(); if (! newEntry.conformsToSchema(null, false, true, true, invalidReason)) { throw new DirectoryException(ResultCode.OBJECTCLASS_VIOLATION, ERR_MODDN_PREOP_VIOLATES_SCHEMA.get(entryDN, invalidReason)); } } } /** * Handle conflict resolution. * @return {@code true} if processing should continue for the operation, or * {@code false} if not. */ private boolean handleConflictResolution() { for (SynchronizationProvider provider : getSynchronizationProviders()) { try { if (!processOperationResult(this, provider.handleConflictResolution(this))) { return false; } } catch (DirectoryException de) { logger.traceException(de); logger.error(ERR_MODDN_SYNCH_CONFLICT_RESOLUTION_FAILED, getConnectionID(), getOperationID(), getExceptionMessage(de)); setResponseData(de); return false; } } return true; } /** * Process pre operation. * @return {@code true} if processing should continue for the operation, or * {@code false} if not. */ private boolean processPreOperation() { for (SynchronizationProvider provider : getSynchronizationProviders()) { try { if (!processOperationResult(this, provider.doPreOperation(this))) { return false; } } catch (DirectoryException de) { logger.traceException(de); logger.error(ERR_MODDN_SYNCH_PREOP_FAILED, getConnectionID(), getOperationID(), getExceptionMessage(de)); setResponseData(de); return false; } } return true; } /** * Invoke post operation synchronization providers. */ private void processSynchPostOperationPlugins() { for (SynchronizationProvider provider : DirectoryServer .getSynchronizationProviders()) { try { provider.doPostOperation(this); } catch (DirectoryException de) { logger.traceException(de); logger.error(ERR_MODDN_SYNCH_POSTOP_FAILED, getConnectionID(), getOperationID(), getExceptionMessage(de)); setResponseData(de); return; } } } }