opends/src/server/org/opends/server/workflowelement/WorkflowElement.java
@@ -40,17 +40,14 @@ */ public abstract class WorkflowElement { // Indicates whether the workflow element encapsulates a private local // backend. private boolean isPrivate = false; // The workflow element identifier. private String workflowElementID = null; /** * Indicates whether the workflow element encapsulates a private * local backend. */ protected boolean isPrivate = false; /** * Creates a new instance of the workflow element. @@ -64,14 +61,14 @@ } /** * Executes the workflow element for an operation. * * @param operation the operation to execute */ public abstract void execute( Operation operation ); public abstract void execute(Operation operation); /** @@ -87,6 +84,21 @@ } /** * Specifies whether the workflow element encapsulates a private local * backend. * * @param isPrivate Indicates whether the workflow element encapsulates a * private local backend. */ protected void setPrivate(boolean isPrivate) { this.isPrivate = isPrivate; } /** * Provides the workflow element identifier. * @@ -97,3 +109,4 @@ return workflowElementID; } } opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendAddOperation.java
@@ -25,28 +25,43 @@ * Portions Copyright 2007 Sun Microsystems, Inc. */ package org.opends.server.workflowelement.localbackend; import org.opends.messages.Message; import org.opends.messages.MessageBuilder; import static org.opends.server.config.ConfigConstants.*; import static org.opends.messages.CoreMessages.*; import static org.opends.server.util.ServerConstants.*; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.locks.Lock; import org.opends.messages.Message; import org.opends.messages.MessageBuilder; import org.opends.server.api.AttributeSyntax; import org.opends.server.api.Backend; import org.opends.server.api.ChangeNotificationListener; import org.opends.server.api.ClientConnection; import org.opends.server.api.PasswordStorageScheme; import org.opends.server.api.PasswordValidator; import org.opends.server.api.SynchronizationProvider; import org.opends.server.api.plugin.PostOperationPluginResult; import org.opends.server.api.plugin.PreOperationPluginResult; import org.opends.server.controls.LDAPAssertionRequestControl; import org.opends.server.controls.LDAPPostReadRequestControl; import org.opends.server.controls.LDAPPostReadResponseControl; import org.opends.server.controls.PasswordPolicyErrorType; import org.opends.server.controls.PasswordPolicyResponseControl; import org.opends.server.controls.ProxiedAuthV1Control; import org.opends.server.controls.ProxiedAuthV2Control; import org.opends.server.core.AccessControlConfigManager; import org.opends.server.core.AddOperation; import org.opends.server.core.AddOperationWrapper; import org.opends.server.core.DirectoryServer; import org.opends.server.core.PasswordPolicy; import org.opends.server.core.PluginConfigManager; import org.opends.server.loggers.debug.DebugTracer; import org.opends.server.protocols.asn1.ASN1OctetString; import org.opends.server.schema.AuthPasswordSyntax; import org.opends.server.schema.BooleanSyntax; @@ -55,31 +70,82 @@ import org.opends.server.types.AttributeType; import org.opends.server.types.AttributeValue; import org.opends.server.types.ByteString; import org.opends.server.types.CancelledOperationException; import org.opends.server.types.CancelResult; import org.opends.server.types.Control; import org.opends.server.types.DebugLogLevel; import org.opends.server.types.DirectoryException; import org.opends.server.types.DN; import org.opends.server.types.Entry; import org.opends.server.types.LDAPException; import org.opends.server.types.LockManager; import org.opends.server.types.ObjectClass; import org.opends.server.types.Privilege; import org.opends.server.types.RDN; import org.opends.server.types.ResultCode; import org.opends.server.types.SearchFilter; import org.opends.server.types.SearchResultEntry; import org.opends.server.types.SynchronizationProviderResult; import org.opends.server.types.operation.PostOperationAddOperation; import org.opends.server.types.operation.PostResponseAddOperation; import org.opends.server.types.operation.PreOperationAddOperation; import org.opends.server.types.operation.PostSynchronizationAddOperation; import org.opends.server.util.TimeThread; import static org.opends.messages.CoreMessages.*; import static org.opends.server.loggers.ErrorLogger.*; import static org.opends.server.loggers.debug.DebugLogger.*; import static org.opends.server.config.ConfigConstants.*; import static org.opends.server.util.ServerConstants.*; import static org.opends.server.util.StaticUtils.*; /** * This class defines an operation used to add an entry in a local backend * of the Directory Server. */ public class LocalBackendAddOperation extends AddOperationWrapper implements PreOperationAddOperation, PostOperationAddOperation, PostResponseAddOperation, PostSynchronizationAddOperation public class LocalBackendAddOperation extends AddOperationWrapper implements PreOperationAddOperation, PostOperationAddOperation, PostResponseAddOperation, PostSynchronizationAddOperation { /** * The tracer object for the debug logger. */ private static final DebugTracer TRACER = getTracer(); // The backend in which the entry is to be added. private Backend backend; // Indicates whether the request includes the LDAP no-op control. private boolean noOp; // Indicates whether to skip post-operation plugin processing. private boolean skipPostOperation; // The DN of the entry to be added. private DN entryDN; // The entry being added to the server. private Entry entry; // The post-read request control included in the request, if applicable. LDAPPostReadRequestControl postReadRequest; // The set of object classes for the entry to add. private Map<ObjectClass, String> objectClasses; // The set of operational attributes for the entry to add. private Map<AttributeType,List<Attribute>> operationalAttributes; // The set of user attributes for the entry to add. private Map<AttributeType,List<Attribute>> userAttributes; /** * Creates a new operation that may be used to add a new entry in a * local backend of the Directory Server. @@ -89,10 +155,12 @@ public LocalBackendAddOperation(AddOperation add) { super(add); LocalBackendWorkflowElement.attachLocalOperation (add, this); } /** * Retrieves the entry to be added to the server. Note that this will not be * available to pre-parse plugins or during the conflict resolution portion of @@ -106,34 +174,983 @@ return entry; } /** * Sets the entry to be added to the server. * Process this add operation against a local backend. * * @param entry - The entry to be added to the server, or <CODE>null</CODE> * if it is not yet available. * @param backend The backend in which the add operation should be * processed. */ public final void setEntryToAdd(Entry entry){ this.entry = entry; void processLocalAdd(Backend backend) { this.backend = backend; ClientConnection clientConnection = getClientConnection(); // Get the plugin config manager that will be used for invoking plugins. PluginConfigManager pluginConfigManager = DirectoryServer.getPluginConfigManager(); skipPostOperation = false; // Check for a request to cancel this operation. if (getCancelRequest() != null) { return; } // Create a labeled block of code that we can break out of if a problem is // detected. addProcessing: { // Process the entry DN and set of attributes to convert them from their // raw forms as provided by the client to the forms required for the rest // of the add processing. entryDN = getEntryDN(); if (entryDN == null) { break addProcessing; } objectClasses = getObjectClasses(); userAttributes = getUserAttributes(); operationalAttributes = getOperationalAttributes(); if ((objectClasses == null ) || (userAttributes == null) || (operationalAttributes == null)) { break addProcessing; } // Check for a request to cancel this operation. if (getCancelRequest() != null) { return; } // Grab a read lock on the parent entry, if there is one. We need to do // this to ensure that the parent is not deleted or renamed while this add // is in progress, and we could also need it to check the entry against // a DIT structure rule. Lock parentLock = null; Lock entryLock = null; DN parentDN = entryDN.getParentDNInSuffix(); try { parentLock = lockParent(parentDN); } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } setResponseData(de); break addProcessing; } try { // Check for a request to cancel this operation. if (getCancelRequest() != null) { return; } // Grab a write lock on the target entry. We'll need to do this // eventually anyway, and we want to make sure that the two locks are // always released when exiting this method, no matter what. Since // the entry shouldn't exist yet, locking earlier than necessary // shouldn't cause a problem. for (int i=0; i < 3; i++) { entryLock = LockManager.lockWrite(entryDN); if (entryLock != null) { break; } } if (entryLock == null) { setResultCode(DirectoryServer.getServerErrorResultCode()); appendErrorMessage(ERR_ADD_CANNOT_LOCK_ENTRY.get( String.valueOf(entryDN))); skipPostOperation = true; break addProcessing; } // Invoke any conflict resolution processing that might be needed by the // synchronization provider. for (SynchronizationProvider provider : DirectoryServer.getSynchronizationProviders()) { try { SynchronizationProviderResult result = provider.handleConflictResolution(this); if (! result.continueOperationProcessing()) { break addProcessing; } } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } logError(ERR_ADD_SYNCH_CONFLICT_RESOLUTION_FAILED.get( getConnectionID(), getOperationID(), getExceptionMessage(de))); setResponseData(de); break addProcessing; } } for (AttributeType at : userAttributes.keySet()) { // If the attribute type is marked "NO-USER-MODIFICATION" then fail // unless this is an internal operation or is related to // synchronization in some way. // This must be done before running the password policy code // and any other code that may add attributes marked as // "NO-USER-MODIFICATION" // // Note that doing this checks at this time // of the processing does not make it possible for pre-parse plugins // to add NO-USER-MODIFICATION attributes to the entry. if (at.isNoUserModification()) { if (! (isInternalOperation() || isSynchronizationOperation())) { setResultCode(ResultCode.UNWILLING_TO_PERFORM); appendErrorMessage(ERR_ADD_ATTR_IS_NO_USER_MOD.get( String.valueOf(entryDN), at.getNameOrOID())); break addProcessing; } } } for (AttributeType at : operationalAttributes.keySet()) { if (at.isNoUserModification()) { if (! (isInternalOperation() || isSynchronizationOperation())) { setResultCode(ResultCode.UNWILLING_TO_PERFORM); appendErrorMessage(ERR_ADD_ATTR_IS_NO_USER_MOD.get( String.valueOf(entryDN), at.getNameOrOID())); break addProcessing; } } } // Check to see if the entry already exists. We do this before // checking whether the parent exists to ensure a referral entry // above the parent results in a correct referral. try { if (DirectoryServer.entryExists(entryDN)) { setResultCode(ResultCode.ENTRY_ALREADY_EXISTS); appendErrorMessage(ERR_ADD_ENTRY_ALREADY_EXISTS.get( String.valueOf(entryDN))); break addProcessing; } } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } setResponseData(de); break addProcessing; } // Get the parent entry, if it exists. Entry parentEntry = null; if (parentDN != null) { try { parentEntry = DirectoryServer.getEntry(parentDN); if (parentEntry == null) { DN matchedDN = parentDN.getParentDNInSuffix(); while (matchedDN != null) { try { if (DirectoryServer.entryExists(matchedDN)) { setMatchedDN(matchedDN); break; } } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } break; } matchedDN = matchedDN.getParentDNInSuffix(); } // The parent doesn't exist, so this add can't be successful. setResultCode(ResultCode.NO_SUCH_OBJECT); appendErrorMessage(ERR_ADD_NO_PARENT.get(String.valueOf(entryDN), String.valueOf(parentDN))); break addProcessing; } } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } setResponseData(de); break addProcessing; } } // Check to make sure that all of the RDN attributes are included as // attribute values. If not, then either add them or report an error. try { addRDNAttributesIfNecessary(); } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } setResponseData(de); break addProcessing; } // Check to make sure that all objectclasses have their superior classes // listed in the entry. If not, then add them. HashSet<ObjectClass> additionalClasses = null; for (ObjectClass oc : objectClasses.keySet()) { ObjectClass superiorClass = oc.getSuperiorClass(); if ((superiorClass != null) && (! objectClasses.containsKey(superiorClass))) { if (additionalClasses == null) { additionalClasses = new HashSet<ObjectClass>(); } additionalClasses.add(superiorClass); } } if (additionalClasses != null) { for (ObjectClass oc : additionalClasses) { addObjectClassChain(oc); } } // Create an entry object to encapsulate the set of attributes and // objectclasses. entry = new Entry(entryDN, objectClasses, userAttributes, operationalAttributes); // Check to see if the entry includes a privilege specification. If so, // then the requester must have the PRIVILEGE_CHANGE privilege. AttributeType privType = DirectoryServer.getAttributeType(OP_ATTR_PRIVILEGE_NAME, true); if (entry.hasAttribute(privType) && (! clientConnection.hasPrivilege(Privilege.PRIVILEGE_CHANGE, this))) { appendErrorMessage( ERR_ADD_CHANGE_PRIVILEGE_INSUFFICIENT_PRIVILEGES.get()); setResultCode(ResultCode.INSUFFICIENT_ACCESS_RIGHTS); break addProcessing; } // If it's not a synchronization operation, then check // to see if the entry contains one or more passwords and if they // are valid in accordance with the password policies associated with // the user. Also perform any encoding that might be required by // password storage schemes. if (! isSynchronizationOperation()) { try { handlePasswordPolicy(); } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } setResponseData(de); break addProcessing; } } // If the server is configured to check schema and the // operation is not a synchronization operation, // check to see if the entry is valid according to the server schema, // and also whether its attributes are valid according to their syntax. if ((DirectoryServer.checkSchema()) && (! isSynchronizationOperation())) { try { checkSchema(parentEntry); } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } setResponseData(de); break addProcessing; } } // Get the backend in which the add is to be performed. if (backend == null) { setResultCode(ResultCode.NO_SUCH_OBJECT); appendErrorMessage(Message.raw("No backend for entry " + entryDN.toString())); // TODO: i18n break addProcessing; } // Check to see if there are any controls in the request. If so, then // see if there is any special processing required. try { processControls(parentDN); } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } setResponseData(de); break addProcessing; } // Check to see if the client has permission to perform the add. // 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 already exists or // if the parent entry does not exist may have already exposed // sensitive information to the client. if (AccessControlConfigManager.getInstance().getAccessControlHandler(). isAllowed(this) == false) { setResultCode(ResultCode.INSUFFICIENT_ACCESS_RIGHTS); appendErrorMessage(ERR_ADD_AUTHZ_INSUFFICIENT_ACCESS_RIGHTS.get( String.valueOf(entryDN))); skipPostOperation = true; break addProcessing; } // Check for a request to cancel this operation. if (getCancelRequest() != null) { return; } // If the operation is not a synchronization operation, // Invoke the pre-operation add plugins. if (! isSynchronizationOperation()) { PreOperationPluginResult preOpResult = pluginConfigManager.invokePreOperationAddPlugins(this); if (preOpResult.connectionTerminated()) { // There's no point in continuing with anything. Log the result // and return. setResultCode(ResultCode.CANCELED); appendErrorMessage(ERR_CANCELED_BY_PREOP_DISCONNECT.get()); return; } else if (preOpResult.sendResponseImmediately()) { skipPostOperation = true; break addProcessing; } else if (preOpResult.skipCoreProcessing()) { skipPostOperation = false; break addProcessing; } } // Check for a request to cancel this operation. if (getCancelRequest() != null) { return; } // If it is not a private backend, then check to see if the server or // backend is operating in read-only mode. if (! backend.isPrivateBackend()) { switch (DirectoryServer.getWritabilityMode()) { case DISABLED: setResultCode(ResultCode.UNWILLING_TO_PERFORM); appendErrorMessage(ERR_ADD_SERVER_READONLY.get( String.valueOf(entryDN))); break addProcessing; case INTERNAL_ONLY: if (! (isInternalOperation() || isSynchronizationOperation())) { setResultCode(ResultCode.UNWILLING_TO_PERFORM); appendErrorMessage(ERR_ADD_SERVER_READONLY.get( String.valueOf(entryDN))); break addProcessing; } break; } switch (backend.getWritabilityMode()) { case DISABLED: setResultCode(ResultCode.UNWILLING_TO_PERFORM); appendErrorMessage(ERR_ADD_BACKEND_READONLY.get( String.valueOf(entryDN))); break addProcessing; case INTERNAL_ONLY: if (! (isInternalOperation() || isSynchronizationOperation())) { setResultCode(ResultCode.UNWILLING_TO_PERFORM); appendErrorMessage(ERR_ADD_BACKEND_READONLY.get( String.valueOf(entryDN))); break addProcessing; } break; } } try { if (noOp) { appendErrorMessage(INFO_ADD_NOOP.get()); setResultCode(ResultCode.NO_OPERATION); } else { for (SynchronizationProvider provider : DirectoryServer.getSynchronizationProviders()) { try { SynchronizationProviderResult result = provider.doPreOperation(this); if (! result.continueOperationProcessing()) { break addProcessing; } } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } logError(ERR_ADD_SYNCH_PREOP_FAILED.get(getConnectionID(), getOperationID(), getExceptionMessage(de))); setResponseData(de); break addProcessing; } } backend.addEntry(entry, this); } if (postReadRequest != null) { addPostReadResponse(); } if (! noOp) { setResultCode(ResultCode.SUCCESS); } } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } setResponseData(de); break addProcessing; } catch (CancelledOperationException coe) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, coe); } CancelResult cancelResult = coe.getCancelResult(); setCancelResult(cancelResult); setResultCode(cancelResult.getResultCode()); Message message = coe.getMessageObject(); if ((message != null) && (message.length() > 0)) { appendErrorMessage(message); } break addProcessing; } } finally { if (entryLock != null) { LockManager.unlock(entryDN, entryLock); } if (parentLock != null) { LockManager.unlock(parentDN, parentLock); } for (SynchronizationProvider provider : DirectoryServer.getSynchronizationProviders()) { try { provider.doPostOperation(this); } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } logError(ERR_ADD_SYNCH_POSTOP_FAILED.get(getConnectionID(), getOperationID(), getExceptionMessage(de))); setResponseData(de); break; } } } } // Indicate that it is now too late to attempt to cancel the operation. setCancelResult(CancelResult.TOO_LATE); // Invoke the post-operation or post-synchronization add plugins. if (isSynchronizationOperation()) { if (getResultCode() == ResultCode.SUCCESS) { pluginConfigManager.invokePostSynchronizationAddPlugins(this); } } else if (! skipPostOperation) { // FIXME -- Should this also be done while holding the locks? PostOperationPluginResult postOpResult = pluginConfigManager.invokePostOperationAddPlugins(this); if (postOpResult.connectionTerminated()) { // There's no point in continuing with anything. Log the result and // return. setResultCode(ResultCode.CANCELED); appendErrorMessage(ERR_CANCELED_BY_PREOP_DISCONNECT.get()); return; } } // Notify any change notification listeners that might be registered with // the server. if ((getResultCode() == ResultCode.SUCCESS) && (entry != null)) { for (ChangeNotificationListener changeListener : DirectoryServer.getChangeNotificationListeners()) { try { changeListener.handleAddOperation(this, entry); } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } logError(ERR_ADD_ERROR_NOTIFYING_CHANGE_LISTENER.get( getExceptionMessage(e))); } } } } /** * Acquire a read lock on the parent of the entry to add. * * @return The acquired read lock. * * @throws DirectoryException If a problem occurs while attempting to * acquire the lock. */ private Lock lockParent(DN parentDN) throws DirectoryException { Lock parentLock = null; if (parentDN == null) { // Either this entry is a suffix or doesn't belong in the directory. if (DirectoryServer.isNamingContext(entryDN)) { // This is fine. This entry is one of the configured suffixes. parentLock = null; } else if (entryDN.isNullDN()) { // This is not fine. The root DSE cannot be added. throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_ADD_CANNOT_ADD_ROOT_DSE.get()); } else { // The entry doesn't have a parent but isn't a suffix. This is not // allowed. throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, ERR_ADD_ENTRY_NOT_SUFFIX.get( String.valueOf(entryDN))); } } else { for (int i=0; i < 3; i++) { parentLock = LockManager.lockRead(parentDN); if (parentLock != null) { break; } } if (parentLock == null) { throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), ERR_ADD_CANNOT_LOCK_PARENT.get( String.valueOf(entryDN), String.valueOf(parentDN))); } } return parentLock; } /** * Adds any missing RDN attributes to the entry. * * @throws DirectoryException If the entry is missing one or more RDN * attributes and the server is configured to * reject such entries. */ private void addRDNAttributesIfNecessary() throws DirectoryException { RDN rdn = entryDN.getRDN(); int numAVAs = rdn.getNumValues(); for (int i=0; i < numAVAs; i++) { AttributeType t = rdn.getAttributeType(i); AttributeValue v = rdn.getAttributeValue(i); String n = rdn.getAttributeName(i); if (t.isOperational()) { List<Attribute> attrList = operationalAttributes.get(t); if (attrList == null) { if (isSynchronizationOperation() || DirectoryServer.addMissingRDNAttributes()) { LinkedHashSet<AttributeValue> valueList = new LinkedHashSet<AttributeValue>(1); valueList.add(v); attrList = new ArrayList<Attribute>(); attrList.add(new Attribute(t, n, valueList)); operationalAttributes.put(t, attrList); } else { throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_ADD_MISSING_RDN_ATTRIBUTE.get( String.valueOf(entryDN), n)); } } else { boolean found = false; for (Attribute a : attrList) { if (a.hasOptions()) { continue; } else { if (! a.hasValue(v)) { a.getValues().add(v); } found = true; break; } } if (! found) { if (isSynchronizationOperation() || DirectoryServer.addMissingRDNAttributes()) { LinkedHashSet<AttributeValue> valueList = new LinkedHashSet<AttributeValue>(1); valueList.add(v); attrList.add(new Attribute(t, n, valueList)); } else { throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_ADD_MISSING_RDN_ATTRIBUTE.get( String.valueOf(entryDN), n)); } } } } else { List<Attribute> attrList = userAttributes.get(t); if (attrList == null) { if (isSynchronizationOperation() || DirectoryServer.addMissingRDNAttributes()) { LinkedHashSet<AttributeValue> valueList = new LinkedHashSet<AttributeValue>(1); valueList.add(v); attrList = new ArrayList<Attribute>(); attrList.add(new Attribute(t, n, valueList)); userAttributes.put(t, attrList); } else { throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_ADD_MISSING_RDN_ATTRIBUTE.get( String.valueOf(entryDN),n)); } } else { boolean found = false; for (Attribute a : attrList) { if (a.hasOptions()) { continue; } else { if (! a.hasValue(v)) { a.getValues().add(v); } found = true; break; } } if (! found) { if (isSynchronizationOperation() || DirectoryServer.addMissingRDNAttributes()) { LinkedHashSet<AttributeValue> valueList = new LinkedHashSet<AttributeValue>(1); valueList.add(v); attrList.add(new Attribute(t, n, valueList)); } else { throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_ADD_MISSING_RDN_ATTRIBUTE.get( String.valueOf(entryDN),n)); } } } } } } /** * Adds the provided objectClass to the entry, along with its superior classes * if appropriate. * * @param objectClass The objectclass to add to the entry. */ public final void addObjectClassChain(ObjectClass objectClass) { Map<ObjectClass, String> objectClasses = getObjectClasses(); if (objectClasses != null){ if (! objectClasses.containsKey(objectClass)) { objectClasses.put(objectClass, objectClass.getNameOrOID()); } ObjectClass superiorClass = objectClass.getSuperiorClass(); if ((superiorClass != null) && (! objectClasses.containsKey(superiorClass))) { addObjectClassChain(superiorClass); } } } /** * Performs all password policy processing necessary for the provided add * operation. * * @param passwordPolicy The password policy associated with the entry to be * added. * @param userEntry The user entry being added. * * @throws DirectoryException If a problem occurs while performing password * policy processing for the add operation. */ public final void handlePasswordPolicy(PasswordPolicy passwordPolicy, Entry userEntry) public final void handlePasswordPolicy() throws DirectoryException { // FIXME -- We need to check to see if the password policy subentry // might be specified virtually rather than as a real // attribute. PasswordPolicy passwordPolicy = null; List<Attribute> pwAttrList = entry.getAttribute(OP_ATTR_PWPOLICY_POLICY_DN); if ((pwAttrList != null) && (! pwAttrList.isEmpty())) { Attribute a = pwAttrList.get(0); LinkedHashSet<AttributeValue> valueSet = a.getValues(); Iterator<AttributeValue> iterator = valueSet.iterator(); if (iterator.hasNext()) { DN policyDN; try { policyDN = DN.decode(iterator.next().getValue()); } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, ERR_ADD_INVALID_PWPOLICY_DN_SYNTAX.get( String.valueOf(entryDN), de.getMessageObject())); } passwordPolicy = DirectoryServer.getPasswordPolicy(policyDN); if (passwordPolicy == null) { throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_ADD_NO_SUCH_PWPOLICY.get( String.valueOf(entryDN), String.valueOf(policyDN))); } } } if (passwordPolicy == null) { passwordPolicy = DirectoryServer.getDefaultPasswordPolicy(); } // See if a password was specified. AttributeType passwordAttribute = passwordPolicy.getPasswordAttribute(); List<Attribute> attrList = userEntry.getAttribute(passwordAttribute); List<Attribute> attrList = entry.getAttribute(passwordAttribute); if ((attrList == null) || attrList.isEmpty()) { // The entry doesn't have a password, so no action is required. @@ -237,7 +1254,7 @@ passwordPolicy.getPasswordValidators().values()) { if (! validator.passwordIsAcceptable(value, currentPasswords, this, userEntry, invalidReason)) entry, invalidReason)) { addPWPolicyControl( PasswordPolicyErrorType.INSUFFICIENT_PASSWORD_QUALITY); @@ -296,7 +1313,7 @@ OP_ATTR_PWPOLICY_CHANGED_TIME, changedTimeValues)); userEntry.putAttribute(changedTimeType, changedTimeList); entry.putAttribute(changedTimeType, changedTimeList); // If we should force change on add, then set the appropriate flag. @@ -319,10 +1336,12 @@ ArrayList<Attribute> resetList = new ArrayList<Attribute>(1); resetList.add(new Attribute(resetType, OP_ATTR_PWPOLICY_RESET_REQUIRED, resetValues)); userEntry.putAttribute(resetType, resetList); entry.putAttribute(resetType, resetList); } } /** * Adds a password policy response control if the corresponding request * control was included. @@ -341,28 +1360,462 @@ } } /** * Adds the provided objectClass to the entry, along with its superior classes * if appropriate. * * @param objectClass The objectclass to add to the entry. */ public final void addObjectClassChain(ObjectClass objectClass) { Map<ObjectClass, String> objectClasses = getObjectClasses(); if (objectClasses != null){ if (! objectClasses.containsKey(objectClass)) { objectClasses.put(objectClass, objectClass.getNameOrOID()); } ObjectClass superiorClass = objectClass.getSuperiorClass(); if ((superiorClass != null) && (! objectClasses.containsKey(superiorClass))) /** * Verifies that the entry to be added conforms to the server schema. * * @param parentEntry The parent of the entry to add. * * @throws DirectoryException If the entry violates the server schema * configuration. */ private void checkSchema(Entry parentEntry) throws DirectoryException { MessageBuilder invalidReason = new MessageBuilder(); if (! entry.conformsToSchema(parentEntry, true, true, true, invalidReason)) { throw new DirectoryException(ResultCode.OBJECTCLASS_VIOLATION, invalidReason.toMessage()); } else { switch (DirectoryServer.getSyntaxEnforcementPolicy()) { addObjectClassChain(superiorClass); case REJECT: invalidReason = new MessageBuilder(); for (List<Attribute> attrList : userAttributes.values()) { for (Attribute a : attrList) { AttributeSyntax syntax = a.getAttributeType().getSyntax(); if (syntax != null) { for (AttributeValue v : a.getValues()) { if (! syntax.valueIsAcceptable(v.getValue(), invalidReason)) { Message message = WARN_ADD_OP_INVALID_SYNTAX.get( String.valueOf(entryDN), String.valueOf(v.getStringValue()), String.valueOf(a.getName()), String.valueOf(invalidReason)); throw new DirectoryException( ResultCode.INVALID_ATTRIBUTE_SYNTAX, message); } } } } } for (List<Attribute> attrList : operationalAttributes.values()) { for (Attribute a : attrList) { AttributeSyntax syntax = a.getAttributeType().getSyntax(); if (syntax != null) { for (AttributeValue v : a.getValues()) { if (! syntax.valueIsAcceptable(v.getValue(), invalidReason)) { Message message = WARN_ADD_OP_INVALID_SYNTAX. get(String.valueOf(entryDN), String.valueOf(v.getStringValue()), String.valueOf(a.getName()), String.valueOf(invalidReason)); throw new DirectoryException( ResultCode.INVALID_ATTRIBUTE_SYNTAX, message); } } } } } break; case WARN: invalidReason = new MessageBuilder(); for (List<Attribute> attrList : userAttributes.values()) { for (Attribute a : attrList) { AttributeSyntax syntax = a.getAttributeType().getSyntax(); if (syntax != null) { for (AttributeValue v : a.getValues()) { if (! syntax.valueIsAcceptable(v.getValue(), invalidReason)) { logError(WARN_ADD_OP_INVALID_SYNTAX.get( String.valueOf(entryDN), String.valueOf(v.getStringValue()), String.valueOf(a.getName()), String.valueOf(invalidReason))); } } } } } for (List<Attribute> attrList : operationalAttributes.values()) { for (Attribute a : attrList) { AttributeSyntax syntax = a.getAttributeType().getSyntax(); if (syntax != null) { for (AttributeValue v : a.getValues()) { if (! syntax.valueIsAcceptable(v.getValue(), invalidReason)) { logError(WARN_ADD_OP_INVALID_SYNTAX.get( String.valueOf(entryDN), String.valueOf(v.getStringValue()), String.valueOf(a.getName()), String.valueOf(invalidReason))); } } } } } break; } } // See if the entry contains any attributes or object classes marked // OBSOLETE. If so, then reject the entry. for (AttributeType at : userAttributes.keySet()) { if (at.isObsolete()) { throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, WARN_ADD_ATTR_IS_OBSOLETE.get( String.valueOf(entryDN), at.getNameOrOID())); } } for (AttributeType at : operationalAttributes.keySet()) { if (at.isObsolete()) { throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, WARN_ADD_ATTR_IS_OBSOLETE.get( String.valueOf(entryDN), at.getNameOrOID())); } } for (ObjectClass oc : objectClasses.keySet()) { if (oc.isObsolete()) { throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, WARN_ADD_OC_IS_OBSOLETE.get( String.valueOf(entryDN), oc.getNameOrOID())); } } } /** * Processes the set of controls contained in the add request. * * @param parentDN The DN of the parent of the entry to add. * * @throws DirectoryException If there is a problem with any of the * request controls. */ private void processControls(DN parentDN) throws DirectoryException { List<Control> requestControls = getRequestControls(); if ((requestControls != null) && (! requestControls.isEmpty())) { for (int i=0; i < requestControls.size(); i++) { Control c = requestControls.get(i); String oid = c.getOID(); if (!AccessControlConfigManager.getInstance(). getAccessControlHandler().isAllowed(parentDN, this, c)) { skipPostOperation = true; throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS, ERR_CONTROL_INSUFFICIENT_ACCESS_RIGHTS.get(oid)); } if (oid.equals(OID_LDAP_ASSERTION)) { LDAPAssertionRequestControl assertControl; if (c instanceof LDAPAssertionRequestControl) { assertControl = (LDAPAssertionRequestControl) c; } else { try { assertControl = LDAPAssertionRequestControl.decodeControl(c); requestControls.set(i, assertControl); } catch (LDAPException le) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, le); } throw new DirectoryException( ResultCode.valueOf(le.getResultCode()), le.getMessageObject()); } } try { // FIXME -- We need to determine whether the current user has // permission to make this determination. SearchFilter filter = assertControl.getSearchFilter(); if (! filter.matchesEntry(entry)) { throw new DirectoryException(ResultCode.ASSERTION_FAILED, ERR_ADD_ASSERTION_FAILED.get( String.valueOf(entryDN))); } } catch (DirectoryException de) { if (de.getResultCode() == ResultCode.ASSERTION_FAILED) { throw de; } if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } throw new DirectoryException(ResultCode.PROTOCOL_ERROR, ERR_ADD_CANNOT_PROCESS_ASSERTION_FILTER.get( String.valueOf(entryDN), de.getMessageObject())); } } else if (oid.equals(OID_LDAP_NOOP_OPENLDAP_ASSIGNED)) { noOp = true; } else if (oid.equals(OID_LDAP_READENTRY_POSTREAD)) { if (c instanceof LDAPPostReadRequestControl) { postReadRequest = (LDAPPostReadRequestControl) c; } else { try { postReadRequest = LDAPPostReadRequestControl.decodeControl(c); requestControls.set(i, postReadRequest); } catch (LDAPException le) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, le); } throw new DirectoryException( ResultCode.valueOf(le.getResultCode()), le.getMessageObject()); } } } else if (oid.equals(OID_PROXIED_AUTH_V1)) { // The requester must have the PROXIED_AUTH privilige in order to // be able to use this control. if (! getClientConnection().hasPrivilege(Privilege.PROXIED_AUTH, this)) { throw new DirectoryException(ResultCode.AUTHORIZATION_DENIED, ERR_PROXYAUTH_INSUFFICIENT_PRIVILEGES.get()); } ProxiedAuthV1Control proxyControl; if (c instanceof ProxiedAuthV1Control) { proxyControl = (ProxiedAuthV1Control) c; } else { try { proxyControl = ProxiedAuthV1Control.decodeControl(c); } catch (LDAPException le) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, le); } throw new DirectoryException( ResultCode.valueOf(le.getResultCode()), le.getMessageObject()); } } Entry authorizationEntry = proxyControl.getAuthorizationEntry(); setAuthorizationEntry(authorizationEntry); if (authorizationEntry == null) { setProxiedAuthorizationDN(DN.nullDN()); } else { setProxiedAuthorizationDN(authorizationEntry.getDN()); } } else if (oid.equals(OID_PROXIED_AUTH_V2)) { // The requester must have the PROXIED_AUTH privilige in order to // be able to use this control. if (! getClientConnection().hasPrivilege(Privilege.PROXIED_AUTH, this)) { throw new DirectoryException(ResultCode.AUTHORIZATION_DENIED, ERR_PROXYAUTH_INSUFFICIENT_PRIVILEGES.get()); } ProxiedAuthV2Control proxyControl; if (c instanceof ProxiedAuthV2Control) { proxyControl = (ProxiedAuthV2Control) c; } else { try { proxyControl = ProxiedAuthV2Control.decodeControl(c); } catch (LDAPException le) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, le); } throw new DirectoryException( ResultCode.valueOf(le.getResultCode()), le.getMessageObject()); } } Entry authorizationEntry = proxyControl.getAuthorizationEntry(); setAuthorizationEntry(authorizationEntry); if (authorizationEntry == null) { setProxiedAuthorizationDN(DN.nullDN()); } else { setProxiedAuthorizationDN(authorizationEntry.getDN()); } } else if (oid.equals(OID_PASSWORD_POLICY_CONTROL)) { // We don't need to do anything here because it's already handled // in LocalBackendAddOperation.handlePasswordPolicy(). } // NYI -- Add support for additional controls. else if (c.isCritical()) { if ((backend == null) || (! backend.supportsControl(oid))) { throw new DirectoryException( ResultCode.UNAVAILABLE_CRITICAL_EXTENSION, ERR_ADD_UNSUPPORTED_CRITICAL_CONTROL.get( String.valueOf(entryDN), oid)); } } } } } /** * Adds the post-read response control to the response. */ private void addPostReadResponse() { Entry addedEntry = entry.duplicate(true); if (! postReadRequest.allowsAttribute( DirectoryServer.getObjectClassAttributeType())) { addedEntry.removeAttribute(DirectoryServer.getObjectClassAttributeType()); } if (! postReadRequest.returnAllUserAttributes()) { Iterator<AttributeType> iterator = addedEntry.getUserAttributes().keySet().iterator(); while (iterator.hasNext()) { AttributeType attrType = iterator.next(); if (! postReadRequest.allowsAttribute(attrType)) { iterator.remove(); } } } if (! postReadRequest.returnAllOperationalAttributes()) { Iterator<AttributeType> iterator = addedEntry.getOperationalAttributes().keySet().iterator(); while (iterator.hasNext()) { AttributeType attrType = iterator.next(); if (! postReadRequest.allowsAttribute(attrType)) { iterator.remove(); } } } // FIXME -- Check access controls on the entry to see if it should // be returned or if any attributes need to be stripped // out.. SearchResultEntry searchEntry = new SearchResultEntry(addedEntry); LDAPPostReadResponseControl responseControl = new LDAPPostReadResponseControl(postReadRequest.getOID(), postReadRequest.isCritical(), searchEntry); addResponseControl(responseControl); } } opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendBindOperation.java
@@ -26,21 +26,146 @@ */ package org.opends.server.workflowelement.localbackend; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.concurrent.locks.Lock; import org.opends.messages.Message; import org.opends.server.admin.std.meta.PasswordPolicyCfgDefn; import org.opends.server.api.Backend; import org.opends.server.api.ClientConnection; import org.opends.server.api.SASLMechanismHandler; import org.opends.server.api.plugin.PostOperationPluginResult; import org.opends.server.api.plugin.PreOperationPluginResult; import org.opends.server.controls.AuthorizationIdentityResponseControl; import org.opends.server.controls.PasswordExpiredControl; import org.opends.server.controls.PasswordExpiringControl; import org.opends.server.controls.PasswordPolicyErrorType; import org.opends.server.controls.PasswordPolicyResponseControl; import org.opends.server.controls.PasswordPolicyWarningType; import org.opends.server.core.AccessControlConfigManager; import org.opends.server.core.BindOperation; import org.opends.server.core.BindOperationWrapper; import org.opends.server.core.DirectoryServer; import org.opends.server.core.PasswordPolicy; import org.opends.server.core.PasswordPolicyState; import org.opends.server.core.PluginConfigManager; import org.opends.server.loggers.debug.DebugTracer; import org.opends.server.types.AccountStatusNotification; import org.opends.server.types.AccountStatusNotificationType; import org.opends.server.types.Attribute; import org.opends.server.types.AttributeType; import org.opends.server.types.AttributeValue; import org.opends.server.types.AuthenticationInfo; import org.opends.server.types.ByteString; import org.opends.server.types.Control; import org.opends.server.types.DebugLogLevel; import org.opends.server.types.DirectoryException; import org.opends.server.types.DN; import org.opends.server.types.Entry; import org.opends.server.types.LockManager; import org.opends.server.types.ResultCode; import org.opends.server.types.WritabilityMode; import org.opends.server.types.operation.PostOperationBindOperation; import org.opends.server.types.operation.PostResponseBindOperation; import org.opends.server.types.operation.PreOperationBindOperation; import static org.opends.messages.CoreMessages.*; 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.util.ServerConstants.*; import static org.opends.server.util.StaticUtils.*; /** * This class defines an operation used to bind against the Directory Server, * with the bound user entry within a local backend. */ public class LocalBackendBindOperation extends BindOperationWrapper implements PreOperationBindOperation, PostOperationBindOperation, PostResponseBindOperation public class LocalBackendBindOperation extends BindOperationWrapper implements PreOperationBindOperation, PostOperationBindOperation, PostResponseBindOperation { /** * The tracer object for the debug logger. */ private static final DebugTracer TRACER = getTracer(); // The backend in which the bind operation should be processed. private Backend backend; // Indicates whether the bind response should include the first warning for an // upcoming password expiration. private boolean isFirstWarning; // Indicates whether this bind is using a grace login for the user. private boolean isGraceLogin; // Indicates whether the user must change his/her password before doing // anything else. private boolean mustChangePassword; // Indicates whether the user requested the password policy control. private boolean pwPolicyControlRequested; // Indicates whether the server should return the authorization ID as a // control in the bind response. private boolean returnAuthzID; // Indicates whether to skip post-operation plugin processing. private boolean skipPostOperation; // The client connection associated with this bind operation. private ClientConnection clientConnection; // The bind DN provided by the client. private DN bindDN; // The entry of the user that successfully authenticated during processing for // this bind operation. private Entry authenticatedUserEntry; // The lookthrough limit that should be enforced for the user. private int lookthroughLimit; // The value to use for the password policy warning. private int pwPolicyWarningValue; // The size limit that should be enforced for the user. private int sizeLimit; // The time limit that should be enforced for the user. private int timeLimit; // The idle time limit that should be enforced for the user. private long idleTimeLimit; // The password policy that applies to the user. private PasswordPolicy policy; // The password policy state for the user. private PasswordPolicyState pwPolicyState; // The password policy error type for this bind operation. private PasswordPolicyErrorType pwPolicyErrorType; // The password policy warning type for this bind operation. private PasswordPolicyWarningType pwPolicyWarningType; // The plugin config manager for the Directory Server. private PluginConfigManager pluginConfigManager; // The SASL mechanism used for this bind operation. private String saslMechanism; /** * Creates a new operation that may be used to bind where @@ -54,4 +179,1131 @@ LocalBackendWorkflowElement.attachLocalOperation (bind, this); } /** * Process this bind operation in a local backend. * * @param backend The backend in which the bind operation should be * processed. */ void processLocalBind(Backend backend) { this.backend = backend; // Initialize a number of variables for use during the bind processing. clientConnection = getClientConnection(); returnAuthzID = false; skipPostOperation = false; sizeLimit = DirectoryServer.getSizeLimit(); timeLimit = DirectoryServer.getTimeLimit(); lookthroughLimit = DirectoryServer.getLookthroughLimit(); idleTimeLimit = DirectoryServer.getIdleTimeLimit(); bindDN = getBindDN(); saslMechanism = getSASLMechanism(); pwPolicyState = null; pwPolicyErrorType = null; pwPolicyControlRequested = false; isGraceLogin = false; isFirstWarning = false; mustChangePassword = false; pwPolicyWarningType = null; pwPolicyWarningValue = -1 ; authenticatedUserEntry = null; pluginConfigManager = DirectoryServer.getPluginConfigManager(); // Create a labeled block of code that we can break out of if a problem is // detected. bindProcessing: { // Check to see if the client has permission to perform the // bind. // FIXME: for now assume that this will check all permission // pertinent to the operation. This includes any controls // specified. if (! AccessControlConfigManager.getInstance().getAccessControlHandler(). isAllowed(this)) { setResultCode(ResultCode.INVALID_CREDENTIALS); setAuthFailureReason(ERR_BIND_AUTHZ_INSUFFICIENT_ACCESS_RIGHTS.get( String.valueOf(bindDN))); skipPostOperation = true; break bindProcessing; } // Check to see if there are any controls in the request. If so, then see // if there is any special processing required. try { handleRequestControls(); } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } setResponseData(de); break bindProcessing; } // Check to see if this is a simple bind or a SASL bind and process // accordingly. switch (getAuthenticationType()) { case SIMPLE: try { if (! processSimpleBind()) { return; } } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } if (de.getResultCode() == ResultCode.INVALID_CREDENTIALS) { setResultCode(ResultCode.INVALID_CREDENTIALS); setAuthFailureReason(de.getMessageObject()); } else { setResponseData(de); } break bindProcessing; } break; case SASL: try { if (! processSASLBind()) { return; } } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } if (de.getResultCode() == ResultCode.INVALID_CREDENTIALS) { setResultCode(ResultCode.INVALID_CREDENTIALS); setAuthFailureReason(de.getMessageObject()); } else { setResponseData(de); } break bindProcessing; } break; default: // Send a protocol error response to the client and disconnect. // NYI return; } } // Update the user's account with any password policy changes that may be // required. try { if (pwPolicyState != null) { pwPolicyState.updateUserEntry(); } } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } setResponseData(de); } // Invoke the post-operation bind plugins. if (! skipPostOperation) { PostOperationPluginResult postOpResult = pluginConfigManager.invokePostOperationBindPlugins(this); if (postOpResult.connectionTerminated()) { // There's no point in continuing with anything. Log the result // and return. setResultCode(ResultCode.CANCELED); appendErrorMessage(ERR_CANCELED_BY_PREOP_DISCONNECT.get()); setProcessingStopTime(); return; } } // Update the authentication information for the user. AuthenticationInfo authInfo = getAuthenticationInfo(); if ((getResultCode() == ResultCode.SUCCESS) && (authInfo != null)) { authenticatedUserEntry = authInfo.getAuthenticationEntry(); clientConnection.setAuthenticationInfo(authInfo); clientConnection.setSizeLimit(sizeLimit); clientConnection.setTimeLimit(timeLimit); clientConnection.setIdleTimeLimit(idleTimeLimit); clientConnection.setLookthroughLimit(lookthroughLimit); clientConnection.setMustChangePassword(mustChangePassword); if (returnAuthzID) { addResponseControl(new AuthorizationIdentityResponseControl( authInfo.getAuthorizationDN())); } } // See if we need to send a password policy control to the client. If so, // then add it to the response. if (getResultCode() == ResultCode.SUCCESS) { if (pwPolicyControlRequested) { PasswordPolicyResponseControl pwpControl = new PasswordPolicyResponseControl(pwPolicyWarningType, pwPolicyWarningValue, pwPolicyErrorType); addResponseControl(pwpControl); } else { if (pwPolicyErrorType == PasswordPolicyErrorType.PASSWORD_EXPIRED) { addResponseControl(new PasswordExpiredControl()); } else if (pwPolicyWarningType == PasswordPolicyWarningType.TIME_BEFORE_EXPIRATION) { addResponseControl(new PasswordExpiringControl(pwPolicyWarningValue)); } } } else { if (pwPolicyControlRequested) { PasswordPolicyResponseControl pwpControl = new PasswordPolicyResponseControl(pwPolicyWarningType, pwPolicyWarningValue, pwPolicyErrorType); addResponseControl(pwpControl); } else { if (pwPolicyErrorType == PasswordPolicyErrorType.PASSWORD_EXPIRED) { addResponseControl(new PasswordExpiredControl()); } } } // Stop the processing timer. setProcessingStopTime(); } /** * Handles request control processing for this bind operation. * * @throws DirectoryException If there is a problem with any of the * controls. */ private void handleRequestControls() throws DirectoryException { List<Control> requestControls = getRequestControls(); if ((requestControls != null) && (! requestControls.isEmpty())) { for (int i=0; i < requestControls.size(); i++) { Control c = requestControls.get(i); String oid = c.getOID(); if (! AccessControlConfigManager.getInstance(). getAccessControlHandler(). isAllowed(bindDN, this, c)) { skipPostOperation = true; throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS, ERR_CONTROL_INSUFFICIENT_ACCESS_RIGHTS.get(oid)); } if (oid.equals(OID_AUTHZID_REQUEST)) { returnAuthzID = true; } else if (oid.equals(OID_PASSWORD_POLICY_CONTROL)) { pwPolicyControlRequested = true; } // NYI -- Add support for additional controls. else if (c.isCritical()) { throw new DirectoryException( ResultCode.UNAVAILABLE_CRITICAL_EXTENSION, ERR_BIND_UNSUPPORTED_CRITICAL_CONTROL.get(oid)); } } } } /** * Performs the processing necessary for a simple bind operation. * * @return {@code true} if processing should continue for the operation, or * {@code false} if not. * * @throws DirectoryException If a problem occurs that should cause the bind * operation to fail. */ private boolean processSimpleBind() throws DirectoryException { // See if this is an anonymous bind. If so, then determine whether // to allow it. ByteString simplePassword = getSimplePassword(); if ((simplePassword == null) || (simplePassword.value().length == 0)) { return processAnonymousSimpleBind(); } // See if the bind DN is actually one of the alternate root DNs // defined in the server. If so, then replace it with the actual DN // for that user. DN actualRootDN = DirectoryServer.getActualRootBindDN(bindDN); if (actualRootDN != null) { bindDN = actualRootDN; } // Get the user entry based on the bind DN. If it does not exist, // then fail. Lock userLock = null; for (int i=0; i < 3; i++) { userLock = LockManager.lockRead(bindDN); if (userLock != null) { break; } } if (userLock == null) { throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), ERR_BIND_OPERATION_CANNOT_LOCK_USER.get( String.valueOf(bindDN))); } try { Entry userEntry; try { userEntry = backend.getEntry(bindDN); } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } userEntry = null; throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, de.getMessageObject()); } if (userEntry == null) { throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, ERR_BIND_OPERATION_UNKNOWN_USER.get( String.valueOf(bindDN))); } else { setUserEntryDN(userEntry.getDN()); } // Check to see if the user has a password. If not, then fail. // FIXME -- We need to have a way to enable/disable debugging. pwPolicyState = new PasswordPolicyState(userEntry, false, false); policy = pwPolicyState.getPolicy(); AttributeType pwType = policy.getPasswordAttribute(); List<Attribute> pwAttr = userEntry.getAttribute(pwType); if ((pwAttr == null) || (pwAttr.isEmpty())) { throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, ERR_BIND_OPERATION_NO_PASSWORD.get( String.valueOf(bindDN))); } // Perform a number of password policy state checks for the user. checkPasswordPolicyState(userEntry, null); // Invoke the pre-operation bind plugins. PreOperationPluginResult preOpResult = pluginConfigManager.invokePreOperationBindPlugins(this); if (preOpResult.connectionTerminated()) { // There's no point in continuing with anything. Log the result // and return. setResultCode(ResultCode.CANCELED); appendErrorMessage(ERR_CANCELED_BY_PREOP_DISCONNECT.get()); setProcessingStopTime(); return false; } else if (preOpResult.sendResponseImmediately() || preOpResult.skipCoreProcessing()) { skipPostOperation = false; return true; } // Determine whether the provided password matches any of the stored // passwords for the user. if (pwPolicyState.passwordMatches(simplePassword)) { setResultCode(ResultCode.SUCCESS); boolean isRoot = DirectoryServer.isRootDN(userEntry.getDN()); if (DirectoryServer.lockdownMode() && (! isRoot)) { throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, ERR_BIND_REJECTED_LOCKDOWN_MODE.get()); } setAuthenticationInfo(new AuthenticationInfo(userEntry, simplePassword, isRoot)); // Set resource limits for the authenticated user. setResourceLimits(userEntry); // Perform any remaining processing for a successful simple // authentication. pwPolicyState.handleDeprecatedStorageSchemes(simplePassword); pwPolicyState.clearFailureLockout(); if (isFirstWarning) { pwPolicyState.setWarnedTime(); int numSeconds = pwPolicyState.getSecondsUntilExpiration(); Message m = WARN_BIND_PASSWORD_EXPIRING.get( secondsToTimeString(numSeconds)); pwPolicyState.generateAccountStatusNotification( AccountStatusNotificationType.PASSWORD_EXPIRING, userEntry, m, AccountStatusNotification.createProperties(pwPolicyState, false, numSeconds, null, null)); } if (isGraceLogin) { pwPolicyState.updateGraceLoginTimes(); } pwPolicyState.setLastLoginTime(); } else { setResultCode(ResultCode.INVALID_CREDENTIALS); setAuthFailureReason(ERR_BIND_OPERATION_WRONG_PASSWORD.get()); if (policy.getLockoutFailureCount() > 0) { pwPolicyState.updateAuthFailureTimes(); if (pwPolicyState.lockedDueToFailures()) { AccountStatusNotificationType notificationType; Message m; boolean tempLocked; int lockoutDuration = pwPolicyState.getSecondsUntilUnlock(); if (lockoutDuration > -1) { notificationType = AccountStatusNotificationType. ACCOUNT_TEMPORARILY_LOCKED; tempLocked = true; m = ERR_BIND_ACCOUNT_TEMPORARILY_LOCKED.get( secondsToTimeString(lockoutDuration)); } else { notificationType = AccountStatusNotificationType. ACCOUNT_PERMANENTLY_LOCKED; tempLocked = false; m = ERR_BIND_ACCOUNT_PERMANENTLY_LOCKED.get(); } pwPolicyState.generateAccountStatusNotification( notificationType, userEntry, m, AccountStatusNotification.createProperties(pwPolicyState, tempLocked, -1, null, null)); } } } return true; } finally { // No matter what, make sure to unlock the user's entry. LockManager.unlock(bindDN, userLock); } } /** * Performs the processing necessary for an anonymous simple bind. * * @throws DirectoryException If a problem occurs that should cause the bind * operation to fail. */ private boolean processAnonymousSimpleBind() throws DirectoryException { // If the server is in lockdown mode, then fail. if (DirectoryServer.lockdownMode()) { throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, ERR_BIND_REJECTED_LOCKDOWN_MODE.get()); } // If there is a bind DN, then see whether that is acceptable. if (DirectoryServer.bindWithDNRequiresPassword() && ((bindDN != null) && (! bindDN.isNullDN()))) { throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_BIND_DN_BUT_NO_PASSWORD.get()); } // Invoke the pre-operation bind plugins. PreOperationPluginResult preOpResult = pluginConfigManager.invokePreOperationBindPlugins(this); if (preOpResult.connectionTerminated()) { // There's no point in continuing with anything. Log the result // and return. setResultCode(ResultCode.CANCELED); appendErrorMessage(ERR_CANCELED_BY_PREOP_DISCONNECT.get()); setProcessingStopTime(); return false; } else if (preOpResult.sendResponseImmediately() || preOpResult.skipCoreProcessing()) { skipPostOperation = true; return true; } setResultCode(ResultCode.SUCCESS); setAuthenticationInfo(new AuthenticationInfo()); return true; } /** * Performs the processing necessary for a SASL bind operation. * * @return {@code true} if processing should continue for the operation, or * {@code false} if not. * * @throws DirectoryException If a problem occurs that should cause the bind * operation to fail. */ private boolean processSASLBind() throws DirectoryException { // Get the appropriate authentication handler for this request based // on the SASL mechanism. If there is none, then fail. SASLMechanismHandler saslHandler = DirectoryServer.getSASLMechanismHandler(saslMechanism); if (saslHandler == null) { throw new DirectoryException(ResultCode.AUTH_METHOD_NOT_SUPPORTED, ERR_BIND_OPERATION_UNKNOWN_SASL_MECHANISM.get( saslMechanism)); } // Check to see if the client has sufficient permission to perform the bind. // NYI // Invoke the pre-operation bind plugins. PreOperationPluginResult preOpResult = pluginConfigManager.invokePreOperationBindPlugins(this); if (preOpResult.connectionTerminated()) { // There's no point in continuing with anything. Log the result // and return. setResultCode(ResultCode.CANCELED); appendErrorMessage(ERR_CANCELED_BY_PREOP_DISCONNECT.get()); setProcessingStopTime(); return false; } else if (preOpResult.sendResponseImmediately() || preOpResult.skipCoreProcessing()) { skipPostOperation = false; return true; } // Actually process the SASL bind. saslHandler.processSASLBind(this); // If the server is operating in lockdown mode, then we will need to // ensure that the authentication was successful and performed as a // root user to continue. Entry saslAuthUserEntry = getSASLAuthUserEntry(); if (DirectoryServer.lockdownMode()) { ResultCode resultCode = getResultCode(); if (resultCode != ResultCode.SASL_BIND_IN_PROGRESS) { if ((resultCode != ResultCode.SUCCESS) || (saslAuthUserEntry == null) || (! DirectoryServer.isRootDN(saslAuthUserEntry.getDN()))) { throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, ERR_BIND_REJECTED_LOCKDOWN_MODE.get()); } } } // Create the password policy state object. if (saslAuthUserEntry == null) { pwPolicyState = null; } else { // FIXME -- Need to have a way to enable debugging. pwPolicyState = new PasswordPolicyState(saslAuthUserEntry, false, false); policy = pwPolicyState.getPolicy(); setUserEntryDN(saslAuthUserEntry.getDN()); // Perform password policy checks that will need to be completed // regardless of whether the authentication was successful. checkPasswordPolicyState(saslAuthUserEntry, saslHandler); } // Determine whether the authentication was successful and perform // any remaining password policy processing accordingly. ResultCode resultCode = getResultCode(); if (resultCode == ResultCode.SUCCESS) { if (pwPolicyState != null) { if (saslHandler.isPasswordBased(saslMechanism) && pwPolicyState.mustChangePassword()) { mustChangePassword = true; } if (isFirstWarning) { pwPolicyState.setWarnedTime(); int numSeconds = pwPolicyState.getSecondsUntilExpiration(); Message m = WARN_BIND_PASSWORD_EXPIRING.get( secondsToTimeString(numSeconds)); pwPolicyState.generateAccountStatusNotification( AccountStatusNotificationType.PASSWORD_EXPIRING, saslAuthUserEntry, m, AccountStatusNotification.createProperties(pwPolicyState, false, numSeconds, null, null)); } if (isGraceLogin) { pwPolicyState.updateGraceLoginTimes(); } pwPolicyState.setLastLoginTime(); // Set appropriate resource limits for the user. setResourceLimits(saslAuthUserEntry); } } else if (resultCode == ResultCode.SASL_BIND_IN_PROGRESS) { // FIXME -- Is any special processing needed here? return false; } else { if (pwPolicyState != null) { if (saslHandler.isPasswordBased(saslMechanism)) { if (pwPolicyState.getPolicy().getLockoutFailureCount() > 0) { pwPolicyState.updateAuthFailureTimes(); if (pwPolicyState.lockedDueToFailures()) { AccountStatusNotificationType notificationType; boolean tempLocked; Message m; int lockoutDuration = pwPolicyState.getSecondsUntilUnlock(); if (lockoutDuration > -1) { notificationType = AccountStatusNotificationType. ACCOUNT_TEMPORARILY_LOCKED; tempLocked = true; m = ERR_BIND_ACCOUNT_TEMPORARILY_LOCKED.get( secondsToTimeString(lockoutDuration)); } else { notificationType = AccountStatusNotificationType.ACCOUNT_PERMANENTLY_LOCKED; tempLocked = false; m = ERR_BIND_ACCOUNT_PERMANENTLY_LOCKED.get(); } pwPolicyState.generateAccountStatusNotification( notificationType, saslAuthUserEntry, m, AccountStatusNotification.createProperties( pwPolicyState, tempLocked, -1, null, null)); } } } } } return true; } /** * Validates a number of password policy state constraints for the user. * * @param userEntry The entry for the user that is authenticating. * @param saslHandler The SASL mechanism handler if this is a SASL bind, or * {@code null} for a simple bind. * * @throws DirectoryException If a problem occurs that should cause the bind * to fail. */ private void checkPasswordPolicyState(Entry userEntry, SASLMechanismHandler saslHandler) throws DirectoryException { boolean isSASLBind = (saslHandler != null); // If the password policy is configured to track authentication failures or // keep the last login time and the associated backend is disabled, then we // may need to reject the bind immediately. if ((policy.getStateUpdateFailurePolicy() == PasswordPolicyCfgDefn.StateUpdateFailurePolicy.PROACTIVE) && ((policy.getLockoutFailureCount() > 0) || ((policy.getLastLoginTimeAttribute() != null) && (policy.getLastLoginTimeFormat() != null))) && ((DirectoryServer.getWritabilityMode() == WritabilityMode.DISABLED) || (backend.getWritabilityMode() == WritabilityMode.DISABLED))) { // This policy isn't applicable to root users, so if it's a root // user then ignore it. if (! DirectoryServer.isRootDN(userEntry.getDN())) { throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, ERR_BIND_OPERATION_WRITABILITY_DISABLED.get( String.valueOf(userEntry.getDN()))); } } // Check to see if the authentication must be done in a secure // manner. If so, then the client connection must be secure. if (policy.requireSecureAuthentication() && (! clientConnection.isSecure())) { if (isSASLBind) { if (! saslHandler.isSecure(saslMechanism)) { throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, ERR_BIND_OPERATION_INSECURE_SASL_BIND.get( saslMechanism, String.valueOf(userEntry.getDN()))); } } else { throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, ERR_BIND_OPERATION_INSECURE_SIMPLE_BIND.get( String.valueOf(userEntry.getDN()))); } } // Check to see if the user is administratively disabled or locked. if (pwPolicyState.isDisabled()) { throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, ERR_BIND_OPERATION_ACCOUNT_DISABLED.get( String.valueOf(userEntry.getDN()))); } else if (pwPolicyState.isAccountExpired()) { Message m = ERR_BIND_OPERATION_ACCOUNT_EXPIRED.get( String.valueOf(userEntry.getDN())); pwPolicyState.generateAccountStatusNotification( AccountStatusNotificationType.ACCOUNT_EXPIRED, userEntry, m, AccountStatusNotification.createProperties(pwPolicyState, false, -1, null, null)); throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, m); } else if (pwPolicyState.lockedDueToFailures()) { if (pwPolicyErrorType == null) { pwPolicyErrorType = PasswordPolicyErrorType.ACCOUNT_LOCKED; } throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, ERR_BIND_OPERATION_ACCOUNT_FAILURE_LOCKED.get( String.valueOf(userEntry.getDN()))); } else if (pwPolicyState.lockedDueToIdleInterval()) { Message m = ERR_BIND_OPERATION_ACCOUNT_IDLE_LOCKED.get( String.valueOf(userEntry.getDN())); if (pwPolicyErrorType == null) { pwPolicyErrorType = PasswordPolicyErrorType.ACCOUNT_LOCKED; } pwPolicyState.generateAccountStatusNotification( AccountStatusNotificationType.ACCOUNT_IDLE_LOCKED, userEntry, m, AccountStatusNotification.createProperties(pwPolicyState, false, -1, null, null)); throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, m); } // If it's a simple bind, or if it's a password-based SASL bind, then // perform a number of password-based checks. if ((! isSASLBind) || saslHandler.isPasswordBased(saslMechanism)) { // Check to see if the account is locked due to the maximum reset age. if (pwPolicyState.lockedDueToMaximumResetAge()) { Message m = ERR_BIND_OPERATION_ACCOUNT_RESET_LOCKED.get( String.valueOf(userEntry.getDN())); if (pwPolicyErrorType == null) { pwPolicyErrorType = PasswordPolicyErrorType.ACCOUNT_LOCKED; } pwPolicyState.generateAccountStatusNotification( AccountStatusNotificationType.ACCOUNT_RESET_LOCKED, userEntry, m, AccountStatusNotification.createProperties(pwPolicyState, false, -1, null, null)); throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, m); } // Determine whether the password is expired, or whether the user // should be warned about an upcoming expiration. if (pwPolicyState.isPasswordExpired()) { if (pwPolicyErrorType == null) { pwPolicyErrorType = PasswordPolicyErrorType.PASSWORD_EXPIRED; } int maxGraceLogins = policy.getGraceLoginCount(); if ((maxGraceLogins > 0) && pwPolicyState.mayUseGraceLogin()) { List<Long> graceLoginTimes = pwPolicyState.getGraceLoginTimes(); if ((graceLoginTimes == null) || (graceLoginTimes.size() < maxGraceLogins)) { isGraceLogin = true; mustChangePassword = true; if (pwPolicyWarningType == null) { pwPolicyWarningType = PasswordPolicyWarningType.GRACE_LOGINS_REMAINING; pwPolicyWarningValue = maxGraceLogins - (graceLoginTimes.size() + 1); } } else { Message m = ERR_BIND_OPERATION_PASSWORD_EXPIRED.get( String.valueOf(userEntry.getDN())); pwPolicyState.generateAccountStatusNotification( AccountStatusNotificationType.PASSWORD_EXPIRED, userEntry, m, AccountStatusNotification.createProperties(pwPolicyState, false, -1, null, null)); throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, m); } } else { Message m = ERR_BIND_OPERATION_PASSWORD_EXPIRED.get( String.valueOf(userEntry.getDN())); pwPolicyState.generateAccountStatusNotification( AccountStatusNotificationType.PASSWORD_EXPIRED, userEntry, m, AccountStatusNotification.createProperties(pwPolicyState, false, -1, null, null)); throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, m); } } else if (pwPolicyState.shouldWarn()) { int numSeconds = pwPolicyState.getSecondsUntilExpiration(); if (pwPolicyWarningType == null) { pwPolicyWarningType = PasswordPolicyWarningType.TIME_BEFORE_EXPIRATION; pwPolicyWarningValue = numSeconds; } isFirstWarning = pwPolicyState.isFirstWarning(); } // Check to see if the user's password has been reset. if (pwPolicyState.mustChangePassword()) { mustChangePassword = true; if (pwPolicyErrorType == null) { pwPolicyErrorType = PasswordPolicyErrorType.CHANGE_AFTER_RESET; } } } } /** * Sets resource limits for the authenticated user. * * @param userEntry The entry for the authenticated user. */ private void setResourceLimits(Entry userEntry) { // See if the user's entry contains a custom size limit. AttributeType attrType = DirectoryServer.getAttributeType(OP_ATTR_USER_SIZE_LIMIT, true); List<Attribute> attrList = userEntry.getAttribute(attrType); if ((attrList != null) && (attrList.size() == 1)) { Attribute a = attrList.get(0); LinkedHashSet<AttributeValue> values = a.getValues(); Iterator<AttributeValue> iterator = values.iterator(); if (iterator.hasNext()) { AttributeValue v = iterator.next(); if (iterator.hasNext()) { logError(WARN_BIND_MULTIPLE_USER_SIZE_LIMITS.get( String.valueOf(userEntry.getDN()))); } else { try { sizeLimit = Integer.parseInt(v.getStringValue()); } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } logError(WARN_BIND_CANNOT_PROCESS_USER_SIZE_LIMIT.get( v.getStringValue(), String.valueOf(userEntry.getDN()))); } } } } // See if the user's entry contains a custom time limit. attrType = DirectoryServer.getAttributeType(OP_ATTR_USER_TIME_LIMIT, true); attrList = userEntry.getAttribute(attrType); if ((attrList != null) && (attrList.size() == 1)) { Attribute a = attrList.get(0); LinkedHashSet<AttributeValue> values = a.getValues(); Iterator<AttributeValue> iterator = values.iterator(); if (iterator.hasNext()) { AttributeValue v = iterator.next(); if (iterator.hasNext()) { logError(WARN_BIND_MULTIPLE_USER_TIME_LIMITS.get( String.valueOf(userEntry.getDN()))); } else { try { timeLimit = Integer.parseInt(v.getStringValue()); } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } logError(WARN_BIND_CANNOT_PROCESS_USER_TIME_LIMIT.get( v.getStringValue(), String.valueOf(userEntry.getDN()))); } } } } // See if the user's entry contains a custom idle time limit. attrType = DirectoryServer.getAttributeType(OP_ATTR_USER_IDLE_TIME_LIMIT, true); attrList = userEntry.getAttribute(attrType); if ((attrList != null) && (attrList.size() == 1)) { Attribute a = attrList.get(0); LinkedHashSet<AttributeValue> values = a.getValues(); Iterator<AttributeValue> iterator = values.iterator(); if (iterator.hasNext()) { AttributeValue v = iterator.next(); if (iterator.hasNext()) { logError(WARN_BIND_MULTIPLE_USER_IDLE_TIME_LIMITS.get( String.valueOf(userEntry.getDN()))); } else { try { idleTimeLimit = 1000L * Long.parseLong(v.getStringValue()); } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } logError(WARN_BIND_CANNOT_PROCESS_USER_IDLE_TIME_LIMIT.get( v.getStringValue(), String.valueOf(userEntry.getDN()))); } } } } // See if the user's entry contains a custom lookthrough limit. attrType = DirectoryServer.getAttributeType(OP_ATTR_USER_LOOKTHROUGH_LIMIT, true); attrList = userEntry.getAttribute(attrType); if ((attrList != null) && (attrList.size() == 1)) { Attribute a = attrList.get(0); LinkedHashSet<AttributeValue> values = a.getValues(); Iterator<AttributeValue> iterator = values.iterator(); if (iterator.hasNext()) { AttributeValue v = iterator.next(); if (iterator.hasNext()) { logError(WARN_BIND_MULTIPLE_USER_LOOKTHROUGH_LIMITS.get( String.valueOf(userEntry.getDN()))); } else { try { lookthroughLimit = Integer.parseInt(v.getStringValue()); } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } logError(WARN_BIND_CANNOT_PROCESS_USER_LOOKTHROUGH_LIMIT.get( v.getStringValue(), String.valueOf(userEntry.getDN()))); } } } } } } opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendCompareOperation.java
@@ -27,28 +27,82 @@ package org.opends.server.workflowelement.localbackend; import java.util.HashSet; import java.util.List; import java.util.concurrent.locks.Lock; import org.opends.server.api.Backend; import org.opends.server.api.ClientConnection; import org.opends.server.api.plugin.PostOperationPluginResult; import org.opends.server.api.plugin.PreOperationPluginResult; import org.opends.server.controls.LDAPAssertionRequestControl; import org.opends.server.controls.ProxiedAuthV1Control; import org.opends.server.controls.ProxiedAuthV2Control; import org.opends.server.core.AccessControlConfigManager; import org.opends.server.core.CompareOperation; import org.opends.server.core.CompareOperationWrapper; import org.opends.server.core.DirectoryServer; import org.opends.server.core.PluginConfigManager; import org.opends.server.loggers.debug.DebugTracer; import org.opends.server.types.Attribute; import org.opends.server.types.AttributeType; import org.opends.server.types.AttributeValue; import org.opends.server.types.Control; import org.opends.server.types.DebugLogLevel; import org.opends.server.types.DirectoryException; import org.opends.server.types.DN; import org.opends.server.types.Entry; import org.opends.server.types.LDAPException; import org.opends.server.types.LockManager; import org.opends.server.types.Privilege; import org.opends.server.types.ResultCode; import org.opends.server.types.SearchFilter; import org.opends.server.types.operation.PostOperationCompareOperation; import org.opends.server.types.operation.PostResponseCompareOperation; import org.opends.server.types.operation.PreOperationCompareOperation; import static org.opends.messages.CoreMessages.*; import static org.opends.server.loggers.debug.DebugLogger.*; import static org.opends.server.util.ServerConstants.*; import static org.opends.server.util.StaticUtils.*; /** * This class defines an operation that may be used to determine whether a * specified entry in the Directory Server contains a given attribute-value * pair. */ public class LocalBackendCompareOperation extends CompareOperationWrapper implements PreOperationCompareOperation, PostOperationCompareOperation, PostResponseCompareOperation public class LocalBackendCompareOperation extends CompareOperationWrapper implements PreOperationCompareOperation, PostOperationCompareOperation, PostResponseCompareOperation { /** * The tracer object for the debug logger. */ private static final DebugTracer TRACER = getTracer(); // The backend in which the comparison is to be performed. private Backend backend; // Indicates whether to skip post-operation processing. private boolean skipPostOperation; // The client connection for this operation. private ClientConnection clientConnection; // The DN of the entry to compare. private DN entryDN; // The entry to be compared. private Entry entry = null; /** * Creates a new compare operation based on the provided compare operation. * @@ -61,6 +115,7 @@ } /** * Retrieves the entry to target with the compare operation. * @@ -73,14 +128,505 @@ } /** * Set the entry to target with the compare operation. * Process this compare operation in a local backend. * * @param entry The entry to target with the compare operation. * @param backend The backend in which the compare operation should be * processed. */ public void setEntryToCompare(Entry entry) void processLocalCompare(Backend backend) { this.entry = entry; this.backend = backend; clientConnection = getClientConnection(); skipPostOperation = false; // Get the plugin config manager that will be used for invoking plugins. PluginConfigManager pluginConfigManager = DirectoryServer.getPluginConfigManager(); // Get a reference to the client connection ClientConnection clientConnection = getClientConnection(); // Check for a request to cancel this operation. if (getCancelRequest() != null) { return; } // Create a labeled block of code that we can break out of if a problem is // detected. compareProcessing: { // Process the entry DN to convert it from the raw form to the form // required for the rest of the compare processing. entryDN = getEntryDN(); if (entryDN == null) { skipPostOperation = true; break compareProcessing; } // If the target entry is in the server configuration, then make sure the // requester has the CONFIG_READ privilege. if (DirectoryServer.getConfigHandler().handlesEntry(entryDN) && (! clientConnection.hasPrivilege(Privilege.CONFIG_READ, this))) { appendErrorMessage(ERR_COMPARE_CONFIG_INSUFFICIENT_PRIVILEGES.get()); setResultCode(ResultCode.INSUFFICIENT_ACCESS_RIGHTS); skipPostOperation = true; break compareProcessing; } // Check for a request to cancel this operation. if (getCancelRequest() != null) { return; } // Grab a read lock on the entry. Lock readLock = null; for (int i=0; i < 3; i++) { readLock = LockManager.lockRead(entryDN); if (readLock != null) { break; } } if (readLock == null) { setResultCode(DirectoryServer.getServerErrorResultCode()); appendErrorMessage(ERR_COMPARE_CANNOT_LOCK_ENTRY.get( String.valueOf(entryDN))); skipPostOperation = true; break compareProcessing; } try { // Get the entry. If it does not exist, then fail. try { entry = DirectoryServer.getEntry(entryDN); if (entry == null) { setResultCode(ResultCode.NO_SUCH_OBJECT); appendErrorMessage( ERR_COMPARE_NO_SUCH_ENTRY.get(String.valueOf(entryDN))); // See if one of the entry's ancestors exists. DN parentDN = entryDN.getParentDNInSuffix(); while (parentDN != null) { try { if (DirectoryServer.entryExists(parentDN)) { setMatchedDN(parentDN); break; } } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } break; } parentDN = parentDN.getParentDNInSuffix(); } break compareProcessing; } } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } setResultCode(de.getResultCode()); appendErrorMessage(de.getMessageObject()); break compareProcessing; } // Check to see if there are any controls in the request. If so, then // see if there is any special processing required. try { handleRequestControls(); } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } setResponseData(de); break compareProcessing; } // Check to see if the client has permission to perform the // compare. // 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 already exists may // have already exposed sensitive information to the client. if (! AccessControlConfigManager.getInstance(). getAccessControlHandler().isAllowed(this)) { setResultCode(ResultCode.INSUFFICIENT_ACCESS_RIGHTS); appendErrorMessage(ERR_COMPARE_AUTHZ_INSUFFICIENT_ACCESS_RIGHTS.get( String.valueOf(entryDN))); skipPostOperation = true; break compareProcessing; } // Check for a request to cancel this operation. if (getCancelRequest() != null) { return; } // Invoke the pre-operation compare plugins. PreOperationPluginResult preOpResult = pluginConfigManager.invokePreOperationComparePlugins(this); if (preOpResult.connectionTerminated()) { // There's no point in continuing with anything. Log the request and // result and return. setResultCode(ResultCode.CANCELED); appendErrorMessage(ERR_CANCELED_BY_PREOP_DISCONNECT.get()); setProcessingStopTime(); return; } else if (preOpResult.sendResponseImmediately()) { skipPostOperation = true; break compareProcessing; } else if (preOpResult.skipCoreProcessing()) { skipPostOperation = false; break compareProcessing; } // Get the base attribute type and set of options. String baseName; HashSet<String> options; String rawAttributeType = getRawAttributeType(); int semicolonPos = rawAttributeType.indexOf(';'); if (semicolonPos > 0) { baseName = toLowerCase(rawAttributeType.substring(0, semicolonPos)); options = new HashSet<String>(); int nextPos = rawAttributeType.indexOf(';', semicolonPos+1); while (nextPos > 0) { options.add(rawAttributeType.substring(semicolonPos+1, nextPos)); semicolonPos = nextPos; nextPos = rawAttributeType.indexOf(';', semicolonPos+1); } options.add(rawAttributeType.substring(semicolonPos+1)); } else { baseName = toLowerCase(rawAttributeType); options = null; } // Actually perform the compare operation. AttributeType attrType = getAttributeType(); if (attrType == null) { attrType = DirectoryServer.getAttributeType(baseName, true); setAttributeType(attrType); } List<Attribute> attrList = entry.getAttribute(attrType, options); if ((attrList == null) || attrList.isEmpty()) { setResultCode(ResultCode.NO_SUCH_ATTRIBUTE); if (options == null) { appendErrorMessage(WARN_COMPARE_OP_NO_SUCH_ATTR.get( String.valueOf(entryDN), baseName)); } else { appendErrorMessage(WARN_COMPARE_OP_NO_SUCH_ATTR_WITH_OPTIONS.get( String.valueOf(entryDN), baseName)); } } else { AttributeValue value = new AttributeValue(attrType, getAssertionValue()); boolean matchFound = false; for (Attribute a : attrList) { if (a.hasValue(value)) { matchFound = true; break; } } if (matchFound) { setResultCode(ResultCode.COMPARE_TRUE); } else { setResultCode(ResultCode.COMPARE_FALSE); } } } finally { LockManager.unlock(entryDN, readLock); } } // Check for a request to cancel this operation. if (getCancelRequest() != null) { return; } // Invoke the post-operation compare plugins. if (! skipPostOperation) { PostOperationPluginResult postOperationResult = pluginConfigManager.invokePostOperationComparePlugins(this); if (postOperationResult.connectionTerminated()) { setResultCode(ResultCode.CANCELED); appendErrorMessage(ERR_CANCELED_BY_POSTOP_DISCONNECT.get()); return; } } } /** * Performs any processing required for the controls included in the request. * * @throws DirectoryException If a problem occurs that should prevent the * operation from succeeding. */ private void handleRequestControls() throws DirectoryException { List<Control> requestControls = getRequestControls(); if ((requestControls != null) && (! requestControls.isEmpty())) { for (int i=0; i < requestControls.size(); i++) { Control c = requestControls.get(i); String oid = c.getOID(); if (! AccessControlConfigManager.getInstance(). getAccessControlHandler().isAllowed(entryDN, this, c)) { skipPostOperation = true; throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS, ERR_CONTROL_INSUFFICIENT_ACCESS_RIGHTS.get(oid)); } if (oid.equals(OID_LDAP_ASSERTION)) { LDAPAssertionRequestControl assertControl; if (c instanceof LDAPAssertionRequestControl) { assertControl = (LDAPAssertionRequestControl) c; } else { try { assertControl = LDAPAssertionRequestControl.decodeControl(c); requestControls.set(i, assertControl); } catch (LDAPException le) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, le); } throw new DirectoryException( ResultCode.valueOf(le.getResultCode()), le.getMessageObject()); } } try { // FIXME -- We need to determine whether the current user has // permission to make this determination. SearchFilter filter = assertControl.getSearchFilter(); if (! filter.matchesEntry(entry)) { throw new DirectoryException(ResultCode.ASSERTION_FAILED, ERR_COMPARE_ASSERTION_FAILED.get( String.valueOf(entryDN))); } } catch (DirectoryException de) { if (de.getResultCode() == ResultCode.ASSERTION_FAILED) { throw de; } if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } throw new DirectoryException(ResultCode.PROTOCOL_ERROR, ERR_COMPARE_CANNOT_PROCESS_ASSERTION_FILTER.get( String.valueOf(entryDN), de.getMessageObject())); } } else if (oid.equals(OID_PROXIED_AUTH_V1)) { // The requester must have the PROXIED_AUTH privilige in order to // be able to use this control. if (! clientConnection.hasPrivilege(Privilege.PROXIED_AUTH, this)) { throw new DirectoryException(ResultCode.AUTHORIZATION_DENIED, ERR_PROXYAUTH_INSUFFICIENT_PRIVILEGES.get()); } ProxiedAuthV1Control proxyControl; if (c instanceof ProxiedAuthV1Control) { proxyControl = (ProxiedAuthV1Control) c; } else { try { proxyControl = ProxiedAuthV1Control.decodeControl(c); } catch (LDAPException le) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, le); } throw new DirectoryException( ResultCode.valueOf(le.getResultCode()), le.getMessageObject()); } } Entry authorizationEntry = proxyControl.getAuthorizationEntry(); setAuthorizationEntry(authorizationEntry); if (authorizationEntry == null) { setProxiedAuthorizationDN(DN.nullDN()); } else { setProxiedAuthorizationDN(authorizationEntry.getDN()); } } else if (oid.equals(OID_PROXIED_AUTH_V2)) { // The requester must have the PROXIED_AUTH privilige in order to // be able to use this control. if (! clientConnection.hasPrivilege(Privilege.PROXIED_AUTH, this)) { throw new DirectoryException(ResultCode.AUTHORIZATION_DENIED, ERR_PROXYAUTH_INSUFFICIENT_PRIVILEGES.get()); } ProxiedAuthV2Control proxyControl; if (c instanceof ProxiedAuthV2Control) { proxyControl = (ProxiedAuthV2Control) c; } else { try { proxyControl = ProxiedAuthV2Control.decodeControl(c); } catch (LDAPException le) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, le); } throw new DirectoryException( ResultCode.valueOf(le.getResultCode()), le.getMessageObject()); } } Entry authorizationEntry = proxyControl.getAuthorizationEntry(); setAuthorizationEntry(authorizationEntry); if (authorizationEntry == null) { setProxiedAuthorizationDN(DN.nullDN()); } else { setProxiedAuthorizationDN(authorizationEntry.getDN()); } } // NYI -- Add support for additional controls. else if (c.isCritical()) { if ((backend == null) || (! backend.supportsControl(oid))) { throw new DirectoryException( ResultCode.UNAVAILABLE_CRITICAL_EXTENSION, ERR_COMPARE_UNSUPPORTED_CRITICAL_CONTROL.get( String.valueOf(entryDN), oid)); } } } } } } opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendDeleteOperation.java
@@ -27,27 +27,97 @@ package org.opends.server.workflowelement.localbackend; import java.util.Iterator; import java.util.List; import java.util.concurrent.locks.Lock; import org.opends.messages.Message; import org.opends.server.api.Backend; import org.opends.server.api.ChangeNotificationListener; import org.opends.server.api.ClientConnection; import org.opends.server.api.SynchronizationProvider; import org.opends.server.api.plugin.PostOperationPluginResult; import org.opends.server.api.plugin.PreOperationPluginResult; import org.opends.server.controls.LDAPAssertionRequestControl; 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.AccessControlConfigManager; import org.opends.server.core.DeleteOperationWrapper; import org.opends.server.core.DeleteOperation; import org.opends.server.core.DirectoryServer; import org.opends.server.core.PluginConfigManager; import org.opends.server.loggers.debug.DebugTracer; import org.opends.server.types.AttributeType; import org.opends.server.types.CancelledOperationException; import org.opends.server.types.CancelResult; import org.opends.server.types.Control; import org.opends.server.types.DebugLogLevel; import org.opends.server.types.DirectoryException; import org.opends.server.types.DN; import org.opends.server.types.Entry; import org.opends.server.types.LDAPException; import org.opends.server.types.LockManager; import org.opends.server.types.Privilege; import org.opends.server.types.ResultCode; import org.opends.server.types.SearchFilter; import org.opends.server.types.SearchResultEntry; import org.opends.server.types.SynchronizationProviderResult; import org.opends.server.types.operation.PostOperationDeleteOperation; import org.opends.server.types.operation.PostResponseDeleteOperation; import org.opends.server.types.operation.PreOperationDeleteOperation; import org.opends.server.types.operation.PostSynchronizationDeleteOperation; import static org.opends.messages.CoreMessages.*; import static org.opends.server.loggers.ErrorLogger.*; import static org.opends.server.loggers.debug.DebugLogger.*; import static org.opends.server.util.ServerConstants.*; import static org.opends.server.util.StaticUtils.*; /** * This class defines an operation used to delete an entry in a local backend * of the Directory Server. */ public class LocalBackendDeleteOperation extends DeleteOperationWrapper implements PreOperationDeleteOperation, PostOperationDeleteOperation, PostResponseDeleteOperation, PostSynchronizationDeleteOperation public class LocalBackendDeleteOperation extends DeleteOperationWrapper implements PreOperationDeleteOperation, PostOperationDeleteOperation, PostResponseDeleteOperation, PostSynchronizationDeleteOperation { /** * The tracer object for the debug logger. */ private static final DebugTracer TRACER = getTracer(); // The backend in which the operation is to be processed. private Backend backend; // Indicates whether the LDAP no-op control has been requested. private boolean noOp; // Indicates whether to skip post-operation processing. private boolean skipPostOperation; // The client connection on which this operation was requested. private ClientConnection clientConnection; // The DN of the entry to be deleted. private DN entryDN; // The entry to be deleted. private Entry entry; // The pre-read request control included in the request, if applicable. private LDAPPreReadRequestControl preReadRequest; /** * Creates a new operation that may be used to delete an entry from a * local backend of the Directory Server. @@ -61,6 +131,7 @@ } /** * Retrieves the entry to be deleted. * @@ -72,13 +143,731 @@ return entry; } /** * Sets the entry to be deleted. * Process this delete operation in a local backend. * * @param entry - The entry to be deleted * @param backend The backend in which the delete operation should be * processed. */ public void setEntryToDelete(Entry entry){ this.entry = entry; void processLocalDelete(Backend backend) { this.backend = backend; clientConnection = getClientConnection(); // Get the plugin config manager that will be used for invoking plugins. PluginConfigManager pluginConfigManager = DirectoryServer.getPluginConfigManager(); skipPostOperation = false; // Check for a request to cancel this operation. if (getCancelRequest() != null) { return; } // Create a labeled block of code that we can break out of if a problem is // detected. deleteProcessing: { // Process the entry DN to convert it from its raw form as provided by the // client to the form required for the rest of the delete processing. entryDN = getEntryDN(); if (entryDN == null){ break deleteProcessing; } // Grab a write lock on the entry. Lock entryLock = null; for (int i=0; i < 3; i++) { entryLock = LockManager.lockWrite(entryDN); if (entryLock != null) { break; } } if (entryLock == null) { setResultCode(DirectoryServer.getServerErrorResultCode()); appendErrorMessage(ERR_DELETE_CANNOT_LOCK_ENTRY.get( String.valueOf(entryDN))); break deleteProcessing; } try { // Get the entry to delete. If it doesn't exist, then fail. try { entry = backend.getEntry(entryDN); if (entry == null) { setResultCode(ResultCode.NO_SUCH_OBJECT); appendErrorMessage(ERR_DELETE_NO_SUCH_ENTRY.get( String.valueOf(entryDN))); try { DN parentDN = entryDN.getParentDNInSuffix(); while (parentDN != null) { if (DirectoryServer.entryExists(parentDN)) { setMatchedDN(parentDN); break; } parentDN = parentDN.getParentDNInSuffix(); } } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } } break deleteProcessing; } } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } setResponseData(de); break deleteProcessing; } // Invoke any conflict resolution processing that might be needed by the // synchronization provider. for (SynchronizationProvider provider : DirectoryServer.getSynchronizationProviders()) { try { SynchronizationProviderResult result = provider.handleConflictResolution(this); if (! result.continueOperationProcessing()) { break deleteProcessing; } } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } logError(ERR_DELETE_SYNCH_CONFLICT_RESOLUTION_FAILED.get( getConnectionID(), getOperationID(), getExceptionMessage(de))); setResponseData(de); break deleteProcessing; } } // Check to see if the client has permission to perform the // delete. // Check to see if there are any controls in the request. If so, then // see if there is any special processing required. try { handleRequestControls(); } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } setResponseData(de); break deleteProcessing; } // 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 already exists may // have already exposed sensitive information to the client. if (! AccessControlConfigManager.getInstance(). getAccessControlHandler().isAllowed(this)) { setResultCode(ResultCode.INSUFFICIENT_ACCESS_RIGHTS); appendErrorMessage(ERR_DELETE_AUTHZ_INSUFFICIENT_ACCESS_RIGHTS.get( String.valueOf(entryDN))); skipPostOperation = true; break deleteProcessing; } // Check for a request to cancel this operation. if (getCancelRequest() != null) { return; } // If the operation is not a synchronization operation, // invoke the pre-delete plugins. if (! isSynchronizationOperation()) { PreOperationPluginResult preOpResult = pluginConfigManager.invokePreOperationDeletePlugins(this); if (preOpResult.connectionTerminated()) { // There's no point in continuing with anything. Log the request // and result and return. setResultCode(ResultCode.CANCELED); appendErrorMessage(ERR_CANCELED_BY_PREOP_DISCONNECT.get()); setProcessingStopTime(); return; } else if (preOpResult.sendResponseImmediately() || preOpResult.skipCoreProcessing()) { skipPostOperation = true; break deleteProcessing; } } // Check for a request to cancel this operation. if (getCancelRequest() != null) { return; } // Get the backend to use for the delete. If there is none, then fail. if (backend == null) { setResultCode(ResultCode.NO_SUCH_OBJECT); appendErrorMessage(ERR_DELETE_NO_SUCH_ENTRY.get( String.valueOf(entryDN))); break deleteProcessing; } // If it is not a private backend, then check to see if the server or // backend is operating in read-only mode. if (! backend.isPrivateBackend()) { switch (DirectoryServer.getWritabilityMode()) { case DISABLED: setResultCode(ResultCode.UNWILLING_TO_PERFORM); appendErrorMessage(ERR_DELETE_SERVER_READONLY.get( String.valueOf(entryDN))); break deleteProcessing; case INTERNAL_ONLY: if (! (isInternalOperation() || isSynchronizationOperation())) { setResultCode(ResultCode.UNWILLING_TO_PERFORM); appendErrorMessage(ERR_DELETE_SERVER_READONLY.get( String.valueOf(entryDN))); break deleteProcessing; } } switch (backend.getWritabilityMode()) { case DISABLED: setResultCode(ResultCode.UNWILLING_TO_PERFORM); appendErrorMessage(ERR_DELETE_BACKEND_READONLY.get( String.valueOf(entryDN))); break deleteProcessing; case INTERNAL_ONLY: if (! (isInternalOperation() || isSynchronizationOperation())) { setResultCode(ResultCode.UNWILLING_TO_PERFORM); appendErrorMessage(ERR_DELETE_BACKEND_READONLY.get( String.valueOf(entryDN))); break deleteProcessing; } } } // The selected backend will have the responsibility of making sure that // the entry actually exists and does not have any children (or possibly // handling a subtree delete). But we will need to check if there are // any subordinate backends that should stop us from attempting the // delete. Backend[] subBackends = backend.getSubordinateBackends(); for (Backend b : subBackends) { DN[] baseDNs = b.getBaseDNs(); for (DN dn : baseDNs) { if (dn.isDescendantOf(entryDN)) { setResultCode(ResultCode.NOT_ALLOWED_ON_NONLEAF); appendErrorMessage(ERR_DELETE_HAS_SUB_BACKEND.get( String.valueOf(entryDN), String.valueOf(dn))); break deleteProcessing; } } } // Actually perform the delete. try { if (noOp) { setResultCode(ResultCode.NO_OPERATION); appendErrorMessage(INFO_DELETE_NOOP.get()); } else { for (SynchronizationProvider provider : DirectoryServer.getSynchronizationProviders()) { try { SynchronizationProviderResult result = provider.doPreOperation(this); if (! result.continueOperationProcessing()) { break deleteProcessing; } } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } logError(ERR_DELETE_SYNCH_PREOP_FAILED.get(getConnectionID(), getOperationID(), getExceptionMessage(de))); setResponseData(de); break deleteProcessing; } } backend.deleteEntry(entryDN, this); } processPreReadControl(); if (! noOp) { setResultCode(ResultCode.SUCCESS); } } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } setResponseData(de); break deleteProcessing; } catch (CancelledOperationException coe) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, coe); } CancelResult cancelResult = coe.getCancelResult(); setCancelResult(cancelResult); setResultCode(cancelResult.getResultCode()); Message message = coe.getMessageObject(); if ((message != null) && (message.length() > 0)) { appendErrorMessage(message); } break deleteProcessing; } } finally { LockManager.unlock(entryDN, entryLock); for (SynchronizationProvider provider : DirectoryServer.getSynchronizationProviders()) { try { provider.doPostOperation(this); } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } logError(ERR_DELETE_SYNCH_POSTOP_FAILED.get(getConnectionID(), getOperationID(), getExceptionMessage(de))); setResponseData(de); break; } } } } // Indicate that it is now too late to attempt to cancel the operation. setCancelResult(CancelResult.TOO_LATE); // Invoke the post-operation or post-synchronization delete plugins. if (isSynchronizationOperation()) { if (getResultCode() == ResultCode.SUCCESS) { pluginConfigManager.invokePostSynchronizationDeletePlugins(this); } } else if (! skipPostOperation) { PostOperationPluginResult postOperationResult = pluginConfigManager.invokePostOperationDeletePlugins(this); if (postOperationResult.connectionTerminated()) { setResultCode(ResultCode.CANCELED); appendErrorMessage(ERR_CANCELED_BY_POSTOP_DISCONNECT.get()); setProcessingStopTime(); return; } } // Notify any change notification listeners that might be registered with // the server. if (getResultCode() == ResultCode.SUCCESS) { for (ChangeNotificationListener changeListener : DirectoryServer.getChangeNotificationListeners()) { try { changeListener.handleDeleteOperation(this, entry); } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } Message message = ERR_DELETE_ERROR_NOTIFYING_CHANGE_LISTENER.get( getExceptionMessage(e)); logError(message); } } } // Stop the processing timer. setProcessingStopTime(); } /** * Performs any request control processing needed for this operation. * * @throws DirectoryException If a problem occurs that should cause the * operation to fail. */ private void handleRequestControls() throws DirectoryException { List<Control> requestControls = getRequestControls(); if ((requestControls != null) && (! requestControls.isEmpty())) { for (int i=0; i < requestControls.size(); i++) { Control c = requestControls.get(i); String oid = c.getOID(); if (!AccessControlConfigManager.getInstance(). getAccessControlHandler().isAllowed(entryDN, this, c)) { skipPostOperation = true; throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS, ERR_CONTROL_INSUFFICIENT_ACCESS_RIGHTS.get(oid)); } if (oid.equals(OID_LDAP_ASSERTION)) { LDAPAssertionRequestControl assertControl; if (c instanceof LDAPAssertionRequestControl) { assertControl = (LDAPAssertionRequestControl) c; } else { try { assertControl = LDAPAssertionRequestControl.decodeControl(c); requestControls.set(i, assertControl); } catch (LDAPException le) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, le); } throw new DirectoryException( ResultCode.valueOf(le.getResultCode()), le.getMessageObject()); } } try { // FIXME -- We need to determine whether the current user has // permission to make this determination. SearchFilter filter = assertControl.getSearchFilter(); if (! filter.matchesEntry(entry)) { throw new DirectoryException(ResultCode.ASSERTION_FAILED, ERR_DELETE_ASSERTION_FAILED.get( String.valueOf(entryDN))); } } catch (DirectoryException de) { if (de.getResultCode() == ResultCode.ASSERTION_FAILED) { throw de; } if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } throw new DirectoryException(ResultCode.PROTOCOL_ERROR, ERR_DELETE_CANNOT_PROCESS_ASSERTION_FILTER.get( String.valueOf(entryDN), de.getMessageObject())); } } else if (oid.equals(OID_LDAP_NOOP_OPENLDAP_ASSIGNED)) { noOp = true; } else if (oid.equals(OID_LDAP_READENTRY_PREREAD)) { if (c instanceof LDAPPreReadRequestControl) { preReadRequest = (LDAPPreReadRequestControl) c; } else { try { preReadRequest = LDAPPreReadRequestControl.decodeControl(c); requestControls.set(i, preReadRequest); } catch (LDAPException le) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, le); } throw new DirectoryException( ResultCode.valueOf(le.getResultCode()), le.getMessageObject()); } } } else if (oid.equals(OID_PROXIED_AUTH_V1)) { // The requester must have the PROXIED_AUTH privilige in order to // be able to use this control. if (! clientConnection.hasPrivilege(Privilege.PROXIED_AUTH, this)) { throw new DirectoryException(ResultCode.AUTHORIZATION_DENIED, ERR_PROXYAUTH_INSUFFICIENT_PRIVILEGES.get()); } ProxiedAuthV1Control proxyControl; if (c instanceof ProxiedAuthV1Control) { proxyControl = (ProxiedAuthV1Control) c; } else { try { proxyControl = ProxiedAuthV1Control.decodeControl(c); } catch (LDAPException le) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, le); } throw new DirectoryException( ResultCode.valueOf(le.getResultCode()), le.getMessageObject()); } } Entry authorizationEntry = proxyControl.getAuthorizationEntry(); setAuthorizationEntry(authorizationEntry); if (authorizationEntry == null) { setProxiedAuthorizationDN(DN.nullDN()); } else { setProxiedAuthorizationDN(authorizationEntry.getDN()); } } else if (oid.equals(OID_PROXIED_AUTH_V2)) { // The requester must have the PROXIED_AUTH privilige in order to // be able to use this control. if (! clientConnection.hasPrivilege(Privilege.PROXIED_AUTH, this)) { throw new DirectoryException(ResultCode.AUTHORIZATION_DENIED, ERR_PROXYAUTH_INSUFFICIENT_PRIVILEGES.get()); } ProxiedAuthV2Control proxyControl; if (c instanceof ProxiedAuthV2Control) { proxyControl = (ProxiedAuthV2Control) c; } else { try { proxyControl = ProxiedAuthV2Control.decodeControl(c); } catch (LDAPException le) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, le); } throw new DirectoryException( ResultCode.valueOf(le.getResultCode()), le.getMessageObject()); } } Entry authorizationEntry = proxyControl.getAuthorizationEntry(); setAuthorizationEntry(authorizationEntry); if (authorizationEntry == null) { setProxiedAuthorizationDN(DN.nullDN()); } else { setProxiedAuthorizationDN(authorizationEntry.getDN()); } } // NYI -- Add support for additional controls. else if (c.isCritical()) { if ((backend == null) || (! backend.supportsControl(oid))) { throw new DirectoryException( ResultCode.UNAVAILABLE_CRITICAL_EXTENSION, ERR_DELETE_UNSUPPORTED_CRITICAL_CONTROL.get( String.valueOf(entryDN), oid)); } } } } } /** * Performs any processing needed for the LDAP pre-read control. */ private void processPreReadControl() { if (preReadRequest != null) { Entry entryCopy = entry.duplicate(true); if (! preReadRequest.allowsAttribute( DirectoryServer.getObjectClassAttributeType())) { entryCopy.removeAttribute( DirectoryServer.getObjectClassAttributeType()); } if (! preReadRequest.returnAllUserAttributes()) { Iterator<AttributeType> iterator = entryCopy.getUserAttributes().keySet().iterator(); while (iterator.hasNext()) { AttributeType attrType = iterator.next(); if (! preReadRequest.allowsAttribute(attrType)) { iterator.remove(); } } } if (! preReadRequest.returnAllOperationalAttributes()) { Iterator<AttributeType> iterator = entryCopy.getOperationalAttributes().keySet().iterator(); while (iterator.hasNext()) { AttributeType attrType = iterator.next(); if (! preReadRequest.allowsAttribute(attrType)) { iterator.remove(); } } } // FIXME -- Check access controls on the entry to see if it should // be returned or if any attributes need to be stripped // out.. SearchResultEntry searchEntry = new SearchResultEntry(entryCopy); LDAPPreReadResponseControl responseControl = new LDAPPreReadResponseControl(preReadRequest.getOID(), preReadRequest.isCritical(), searchEntry); addResponseControl(responseControl); } } } opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendModifyDNOperation.java
@@ -26,14 +26,71 @@ */ package org.opends.server.workflowelement.localbackend; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.concurrent.locks.Lock; import org.opends.messages.Message; import org.opends.messages.MessageBuilder; import org.opends.server.api.Backend; import org.opends.server.api.ChangeNotificationListener; import org.opends.server.api.ClientConnection; import org.opends.server.api.SynchronizationProvider; import org.opends.server.api.plugin.PostOperationPluginResult; import org.opends.server.api.plugin.PreOperationPluginResult; import org.opends.server.controls.LDAPAssertionRequestControl; 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.AccessControlConfigManager; import org.opends.server.core.DirectoryServer; import org.opends.server.core.ModifyDNOperation; import org.opends.server.core.ModifyDNOperationWrapper; import org.opends.server.core.PluginConfigManager; import org.opends.server.loggers.debug.DebugTracer; import org.opends.server.protocols.asn1.ASN1OctetString; import org.opends.server.types.Attribute; import org.opends.server.types.AttributeType; import org.opends.server.types.AttributeValue; import org.opends.server.types.ByteString; import org.opends.server.types.CancelledOperationException; import org.opends.server.types.CancelResult; import org.opends.server.types.Control; import org.opends.server.types.DebugLogLevel; import org.opends.server.types.DirectoryException; import org.opends.server.types.DN; import org.opends.server.types.Entry; import org.opends.server.types.LDAPException; import org.opends.server.types.LockManager; import org.opends.server.types.Modification; import org.opends.server.types.ModificationType; import org.opends.server.types.Privilege; import org.opends.server.types.RDN; import org.opends.server.types.ResultCode; import org.opends.server.types.SearchFilter; import org.opends.server.types.SearchResultEntry; import org.opends.server.types.SynchronizationProviderResult; import org.opends.server.types.operation.PostOperationModifyDNOperation; import org.opends.server.types.operation.PostResponseModifyDNOperation; import org.opends.server.types.operation.PreOperationModifyDNOperation; import org.opends.server.types.operation.PostSynchronizationModifyDNOperation; import static org.opends.messages.CoreMessages.*; import static org.opends.server.loggers.ErrorLogger.*; import static org.opends.server.loggers.debug.DebugLogger.*; import static org.opends.server.util.ServerConstants.*; import static org.opends.server.util.StaticUtils.*; /** * This class defines an operation used to move an entry in a local backend * of the Directory Server. @@ -45,12 +102,45 @@ PostResponseModifyDNOperation, PostSynchronizationModifyDNOperation { /** * The tracer object for the debug logger. */ private static final DebugTracer TRACER = getTracer(); // 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; // Indicates whether to skip post-operation plugin processing. private boolean skipPostOperation; // The client connection on which this operation was requested. private ClientConnection clientConnection; // The original DN of the entry. 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. @@ -63,6 +153,8 @@ 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 @@ -76,6 +168,8 @@ 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 @@ -89,31 +183,1246 @@ return newEntry; } /** * Sets 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. * * @param entry The current entry, or <CODE>null</CODE> if it is not yet * available. */ public final void setOriginalEntry(Entry entry) { this.currentEntry = entry; } /** * Sets 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. * Process this modify DN operation in a local backend. * * @param entry The updated entry, or <CODE>null</CODE> if it is not yet * available. * @param backend The backend in which the modify DN operation should be * processed. */ public final void setUpdatedEntry(Entry entry) void processLocalModifyDN(Backend backend) { this.newEntry = entry; this.backend = backend; clientConnection = getClientConnection(); // Get the plugin config manager that will be used for invoking plugins. PluginConfigManager pluginConfigManager = DirectoryServer.getPluginConfigManager(); skipPostOperation = false; // Check for a request to cancel this operation. if (getCancelRequest() != null) { return; } // Create a labeled block of code that we can break out of if a problem is // detected. modifyDNProcessing: { // 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) { skipPostOperation = true; break modifyDNProcessing; } DN newSuperior = getNewSuperior(); if ((newSuperior == null) && (getRawNewSuperior() != null)) { skipPostOperation = true; break modifyDNProcessing; } // Construct the new DN to use for the entry. DN parentDN; if (newSuperior == null) { parentDN = entryDN.getParentDNInSuffix(); } else { parentDN = newSuperior; } if ((parentDN == null) || parentDN.isNullDN()) { setResultCode(ResultCode.UNWILLING_TO_PERFORM); appendErrorMessage(ERR_MODDN_NO_PARENT.get(String.valueOf(entryDN))); break modifyDNProcessing; } DN newDN = parentDN.concat(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( String.valueOf(entryDN))); break modifyDNProcessing; } Backend newBackend = DirectoryServer.getBackend(newDN); if (newBackend == null) { setResultCode(ResultCode.NO_SUCH_OBJECT); appendErrorMessage(ERR_MODDN_NO_BACKEND_FOR_NEW_ENTRY.get( String.valueOf(entryDN), String.valueOf(newDN))); break modifyDNProcessing; } else if (! currentBackend.equals(newBackend)) { setResultCode(ResultCode.UNWILLING_TO_PERFORM); appendErrorMessage(ERR_MODDN_DIFFERENT_BACKENDS.get( String.valueOf(entryDN), String.valueOf(newDN))); break modifyDNProcessing; } // Check for a request to cancel this operation. if (getCancelRequest() != null) { return; } // Acquire write locks for the current and new DN. Lock currentLock = null; for (int i=0; i < 3; i++) { currentLock = LockManager.lockWrite(entryDN); if (currentLock != null) { break; } } if (currentLock == null) { setResultCode(DirectoryServer.getServerErrorResultCode()); appendErrorMessage(ERR_MODDN_CANNOT_LOCK_CURRENT_DN.get( String.valueOf(entryDN))); skipPostOperation = true; break modifyDNProcessing; } Lock newLock = null; try { for (int i=0; i < 3; i++) { newLock = LockManager.lockWrite(newDN); if (newLock != null) { break; } } } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } LockManager.unlock(entryDN, currentLock); if (newLock != null) { LockManager.unlock(newDN, newLock); } setResultCode(DirectoryServer.getServerErrorResultCode()); appendErrorMessage(ERR_MODDN_EXCEPTION_LOCKING_NEW_DN.get( String.valueOf(entryDN), String.valueOf(newDN), getExceptionMessage(e))); skipPostOperation = true; break modifyDNProcessing; } if (newLock == null) { LockManager.unlock(entryDN, currentLock); setResultCode(DirectoryServer.getServerErrorResultCode()); appendErrorMessage(ERR_MODDN_CANNOT_LOCK_NEW_DN.get( String.valueOf(entryDN), String.valueOf(newDN))); skipPostOperation = true; break modifyDNProcessing; } try { // Check for a request to cancel this operation. if (getCancelRequest() != null) { return; } // Get the current entry from the appropriate backend. If it doesn't // exist, then fail. try { currentEntry = currentBackend.getEntry(entryDN); } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } setResponseData(de); break modifyDNProcessing; } if (getOriginalEntry() == null) { // See if one of the entry's ancestors exists. parentDN = entryDN.getParentDNInSuffix(); while (parentDN != null) { try { if (DirectoryServer.entryExists(parentDN)) { setMatchedDN(parentDN); break; } } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } break; } parentDN = parentDN.getParentDNInSuffix(); } setResultCode(ResultCode.NO_SUCH_OBJECT); appendErrorMessage(ERR_MODDN_NO_CURRENT_ENTRY.get( String.valueOf(entryDN))); break modifyDNProcessing; } // Invoke any conflict resolution processing that might be needed by the // synchronization provider. for (SynchronizationProvider provider : DirectoryServer.getSynchronizationProviders()) { try { SynchronizationProviderResult result = provider.handleConflictResolution(this); if (! result.continueOperationProcessing()) { break modifyDNProcessing; } } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } logError(ERR_MODDN_SYNCH_CONFLICT_RESOLUTION_FAILED.get( getConnectionID(), getOperationID(), getExceptionMessage(de))); setResponseData(de); break modifyDNProcessing; } } // Check to see if there are any controls in the request. If so, then // see if there is any special processing required. try { handleRequestControls(); } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } setResponseData(de); break modifyDNProcessing; } // 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. if (! AccessControlConfigManager.getInstance(). getAccessControlHandler().isAllowed(this)) { setResultCode(ResultCode.INSUFFICIENT_ACCESS_RIGHTS); appendErrorMessage(ERR_MODDN_AUTHZ_INSUFFICIENT_ACCESS_RIGHTS.get( String.valueOf(entryDN))); skipPostOperation = true; break modifyDNProcessing; } // 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<Modification> modifications = this.getModifications(); // Apply any changes to the entry based on the change in its RDN. Also, // perform schema checking on the updated entry. try { applyRDNChanges(modifications); } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } setResponseData(de); break modifyDNProcessing; } // Check for a request to cancel this operation. if (getCancelRequest() != null) { return; } // 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(); // If the operation is not a synchronization operation, // Invoke the pre-operation modify DN plugins. if (! isSynchronizationOperation()) { PreOperationPluginResult preOpResult = pluginConfigManager.invokePreOperationModifyDNPlugins(this); if (preOpResult.connectionTerminated()) { // There's no point in continuing with anything. Log the request // and result and return. setResultCode(ResultCode.CANCELED); appendErrorMessage(ERR_CANCELED_BY_PREOP_DISCONNECT.get()); return; } else if (preOpResult.sendResponseImmediately()) { skipPostOperation = true; break modifyDNProcessing; } else if (preOpResult.skipCoreProcessing()) { skipPostOperation = false; break modifyDNProcessing; } } // 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) { try { applyPreOpModifications(modifications, modCount); } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } setResponseData(de); break modifyDNProcessing; } } // Check for a request to cancel this operation. if (getCancelRequest() != null) { return; } // Actually perform the modify DN operation. // This should include taking // care of any synchronization that might be needed. try { // If it is not a private backend, then check to see if the server or // backend is operating in read-only mode. if (! currentBackend.isPrivateBackend()) { switch (DirectoryServer.getWritabilityMode()) { case DISABLED: setResultCode(ResultCode.UNWILLING_TO_PERFORM); appendErrorMessage(ERR_MODDN_SERVER_READONLY.get( String.valueOf(entryDN))); break modifyDNProcessing; case INTERNAL_ONLY: if (! (isInternalOperation() || isSynchronizationOperation())) { setResultCode(ResultCode.UNWILLING_TO_PERFORM); appendErrorMessage(ERR_MODDN_SERVER_READONLY.get( String.valueOf(entryDN))); break modifyDNProcessing; } } switch (currentBackend.getWritabilityMode()) { case DISABLED: setResultCode(ResultCode.UNWILLING_TO_PERFORM); appendErrorMessage(ERR_MODDN_BACKEND_READONLY.get( String.valueOf(entryDN))); break modifyDNProcessing; case INTERNAL_ONLY: if (! (isInternalOperation() || isSynchronizationOperation())) { setResultCode(ResultCode.UNWILLING_TO_PERFORM); appendErrorMessage(ERR_MODDN_BACKEND_READONLY.get( String.valueOf(entryDN))); break modifyDNProcessing; } } } if (noOp) { appendErrorMessage(INFO_MODDN_NOOP.get()); setResultCode(ResultCode.NO_OPERATION); } else { for (SynchronizationProvider provider : DirectoryServer.getSynchronizationProviders()) { try { SynchronizationProviderResult result = provider.doPreOperation(this); if (! result.continueOperationProcessing()) { break modifyDNProcessing; } } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } logError(ERR_MODDN_SYNCH_PREOP_FAILED.get(getConnectionID(), getOperationID(), getExceptionMessage(de))); setResponseData(de); break modifyDNProcessing; } } currentBackend.renameEntry(entryDN, newEntry, this); } // Attach the pre-read and/or post-read controls to the response if // appropriate. processReadEntryControls(); if (! noOp) { setResultCode(ResultCode.SUCCESS); } } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } setResponseData(de); break modifyDNProcessing; } catch (CancelledOperationException coe) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, coe); } CancelResult cancelResult = coe.getCancelResult(); setCancelResult(cancelResult); setResultCode(cancelResult.getResultCode()); Message message = coe.getMessageObject(); if ((message != null) && (message.length() > 0)) { appendErrorMessage(message); } break modifyDNProcessing; } } finally { LockManager.unlock(entryDN, currentLock); LockManager.unlock(newDN, newLock); for (SynchronizationProvider provider : DirectoryServer.getSynchronizationProviders()) { try { provider.doPostOperation(this); } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } logError(ERR_MODDN_SYNCH_POSTOP_FAILED.get(getConnectionID(), getOperationID(), getExceptionMessage(de))); setResponseData(de); break; } } } } // Indicate that it is now too late to attempt to cancel the operation. setCancelResult(CancelResult.TOO_LATE); // Invoke the post-operation or post-synchronization modify DN plugins. if (isSynchronizationOperation()) { if (getResultCode() == ResultCode.SUCCESS) { pluginConfigManager.invokePostSynchronizationModifyDNPlugins(this); } } else if (! skipPostOperation) { PostOperationPluginResult postOperationResult = pluginConfigManager.invokePostOperationModifyDNPlugins(this); if (postOperationResult.connectionTerminated()) { setResultCode(ResultCode.CANCELED); appendErrorMessage(ERR_CANCELED_BY_POSTOP_DISCONNECT.get()); return; } } // Notify any change notification listeners that might be registered with // the server. if (getResultCode() == ResultCode.SUCCESS) { for (ChangeNotificationListener changeListener : DirectoryServer.getChangeNotificationListeners()) { try { changeListener.handleModifyDNOperation(this, currentEntry, newEntry); } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } Message message = ERR_MODDN_ERROR_NOTIFYING_CHANGE_LISTENER.get( getExceptionMessage(e)); logError(message); } } } } /** * 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 { List<Control> requestControls = getRequestControls(); if ((requestControls != null) && (! requestControls.isEmpty())) { for (int i=0; i < requestControls.size(); i++) { Control c = requestControls.get(i); String oid = c.getOID(); if (! AccessControlConfigManager.getInstance(). getAccessControlHandler().isAllowed(entryDN, this, c)) { skipPostOperation = true; throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS, ERR_CONTROL_INSUFFICIENT_ACCESS_RIGHTS.get(oid)); } if (oid.equals(OID_LDAP_ASSERTION)) { LDAPAssertionRequestControl assertControl; if (c instanceof LDAPAssertionRequestControl) { assertControl = (LDAPAssertionRequestControl) c; } else { try { assertControl = LDAPAssertionRequestControl.decodeControl(c); requestControls.set(i, assertControl); } catch (LDAPException le) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, le); } throw new DirectoryException( ResultCode.valueOf(le.getResultCode()), le.getMessageObject()); } } try { // FIXME -- We need to determine whether the current user has // permission to make this determination. SearchFilter filter = assertControl.getSearchFilter(); if (! filter.matchesEntry(currentEntry)) { throw new DirectoryException(ResultCode.ASSERTION_FAILED, ERR_MODDN_ASSERTION_FAILED.get( String.valueOf(entryDN))); } } catch (DirectoryException de) { if (de.getResultCode() == ResultCode.ASSERTION_FAILED) { throw de; } if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } throw new DirectoryException(ResultCode.PROTOCOL_ERROR, ERR_MODDN_CANNOT_PROCESS_ASSERTION_FILTER.get( String.valueOf(entryDN), de.getMessageObject())); } } else if (oid.equals(OID_LDAP_NOOP_OPENLDAP_ASSIGNED)) { noOp = true; } else if (oid.equals(OID_LDAP_READENTRY_PREREAD)) { if (c instanceof LDAPPreReadRequestControl) { preReadRequest = (LDAPPreReadRequestControl) c; } else { try { preReadRequest = LDAPPreReadRequestControl.decodeControl(c); requestControls.set(i, preReadRequest); } catch (LDAPException le) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, le); } throw new DirectoryException( ResultCode.valueOf(le.getResultCode()), le.getMessageObject()); } } } else if (oid.equals(OID_LDAP_READENTRY_POSTREAD)) { if (c instanceof LDAPPostReadRequestControl) { postReadRequest = (LDAPPostReadRequestControl) c; } else { try { postReadRequest = LDAPPostReadRequestControl.decodeControl(c); requestControls.set(i, postReadRequest); } catch (LDAPException le) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, le); } throw new DirectoryException( ResultCode.valueOf(le.getResultCode()), le.getMessageObject()); } } } else if (oid.equals(OID_PROXIED_AUTH_V1)) { // The requester must have the PROXIED_AUTH privilige in order to // be able to use this control. if (! clientConnection.hasPrivilege(Privilege.PROXIED_AUTH, this)) { throw new DirectoryException(ResultCode.AUTHORIZATION_DENIED, ERR_PROXYAUTH_INSUFFICIENT_PRIVILEGES.get()); } ProxiedAuthV1Control proxyControl; if (c instanceof ProxiedAuthV1Control) { proxyControl = (ProxiedAuthV1Control) c; } else { try { proxyControl = ProxiedAuthV1Control.decodeControl(c); } catch (LDAPException le) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, le); } throw new DirectoryException( ResultCode.valueOf(le.getResultCode()), le.getMessageObject()); } } Entry authorizationEntry = proxyControl.getAuthorizationEntry(); setAuthorizationEntry(authorizationEntry); if (authorizationEntry == null) { setProxiedAuthorizationDN(DN.nullDN()); } else { setProxiedAuthorizationDN(authorizationEntry.getDN()); } } else if (oid.equals(OID_PROXIED_AUTH_V2)) { // The requester must have the PROXIED_AUTH privilige in order to // be able to use this control. if (! clientConnection.hasPrivilege(Privilege.PROXIED_AUTH, this)) { throw new DirectoryException(ResultCode.AUTHORIZATION_DENIED, ERR_PROXYAUTH_INSUFFICIENT_PRIVILEGES.get()); } ProxiedAuthV2Control proxyControl; if (c instanceof ProxiedAuthV2Control) { proxyControl = (ProxiedAuthV2Control) c; } else { try { proxyControl = ProxiedAuthV2Control.decodeControl(c); } catch (LDAPException le) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, le); } throw new DirectoryException( ResultCode.valueOf(le.getResultCode()), le.getMessageObject()); } } Entry authorizationEntry = proxyControl.getAuthorizationEntry(); setAuthorizationEntry(authorizationEntry); if (authorizationEntry == null) { setProxiedAuthorizationDN(DN.nullDN()); } else { setProxiedAuthorizationDN(authorizationEntry.getDN()); } } // NYI -- Add support for additional controls. else if (c.isCritical()) { if ((backend == null) || (! backend.supportsControl(oid))) { throw new DirectoryException( ResultCode.UNAVAILABLE_CRITICAL_EXTENSION, ERR_MODDN_UNSUPPORTED_CRITICAL_CONTROL.get( String.valueOf(entryDN), oid)); } } } } } /** * 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<Modification> modifications) throws DirectoryException { // If we should delete the old RDN values from the entry, then do so. if (deleteOldRDN()) { RDN currentRDN = entryDN.getRDN(); int numValues = currentRDN.getNumValues(); for (int i=0; i < numValues; i++) { LinkedHashSet<AttributeValue> valueSet = new LinkedHashSet<AttributeValue>(1); valueSet.add(currentRDN.getAttributeValue(i)); Attribute a = new Attribute(currentRDN.getAttributeType(i), currentRDN.getAttributeName(i), valueSet); // If the associated attribute type is marked NO-USER-MODIFICATION, then // refuse the update. if (a.getAttributeType().isNoUserModification()) { if (! (isInternalOperation() || isSynchronizationOperation())) { throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_MODDN_OLD_RDN_ATTR_IS_NO_USER_MOD.get( String.valueOf(entryDN), a.getName())); } } LinkedList<AttributeValue> missingValues = new LinkedList<AttributeValue>(); newEntry.removeAttribute(a, missingValues); if (missingValues.isEmpty()) { modifications.add(new Modification(ModificationType.DELETE, a)); } } } // Add the new RDN values to the entry. int newRDNValues = newRDN.getNumValues(); for (int i=0; i < newRDNValues; i++) { LinkedHashSet<AttributeValue> valueSet = new LinkedHashSet<AttributeValue>(1); valueSet.add(newRDN.getAttributeValue(i)); Attribute a = new Attribute(newRDN.getAttributeType(i), newRDN.getAttributeName(i), valueSet); LinkedList<AttributeValue> duplicateValues = new LinkedList<AttributeValue>(); newEntry.addAttribute(a, duplicateValues); if (duplicateValues.isEmpty()) { // If the associated attribute type is marked NO-USER-MODIFICATION, then // refuse the update. if (a.getAttributeType().isNoUserModification()) { if (! (isInternalOperation() || isSynchronizationOperation())) { throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_MODDN_NEW_RDN_ATTR_IS_NO_USER_MOD.get( String.valueOf(entryDN), a.getName())); } } 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())) { MessageBuilder invalidReason = new MessageBuilder(); if (! newEntry.conformsToSchema(null, false, true, true, invalidReason)) { throw new DirectoryException(ResultCode.OBJECTCLASS_VIOLATION, ERR_MODDN_VIOLATES_SCHEMA.get( String.valueOf(entryDN), String.valueOf(invalidReason))); } for (int i=0; i < newRDNValues; i++) { AttributeType at = newRDN.getAttributeType(i); if (at.isObsolete()) { throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_MODDN_NEWRDN_ATTR_IS_OBSOLETE.get( String.valueOf(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. * * @throws DirectoryException If a problem occurs that should cause the * modify DN operation to fail. */ private void applyPreOpModifications(List<Modification> modifications, int startPos) throws DirectoryException { for (int i=startPos; i < modifications.size(); i++) { Modification m = modifications.get(i); Attribute a = m.getAttribute(); switch (m.getModificationType()) { case ADD: LinkedList<AttributeValue> duplicateValues = new LinkedList<AttributeValue>(); newEntry.addAttribute(a, duplicateValues); break; case DELETE: LinkedList<AttributeValue> missingValues = new LinkedList<AttributeValue>(); newEntry.removeAttribute(a, missingValues); break; case REPLACE: duplicateValues = new LinkedList<AttributeValue>(); newEntry.removeAttribute(a.getAttributeType(), a.getOptions()); newEntry.addAttribute(a, duplicateValues); break; case INCREMENT: List<Attribute> attrList = newEntry.getAttribute(a.getAttributeType(), a.getOptions()); if ((attrList == null) || attrList.isEmpty()) { throw new DirectoryException(ResultCode.NO_SUCH_ATTRIBUTE, ERR_MODDN_PREOP_INCREMENT_NO_ATTR.get( String.valueOf(entryDN), a.getName())); } else if (attrList.size() > 1) { throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, ERR_MODDN_PREOP_INCREMENT_MULTIPLE_VALUES.get( String.valueOf(entryDN), a.getName())); } LinkedHashSet<AttributeValue> values = attrList.get(0).getValues(); if ((values == null) || values.isEmpty()) { throw new DirectoryException(ResultCode.NO_SUCH_ATTRIBUTE, ERR_MODDN_PREOP_INCREMENT_NO_ATTR.get( String.valueOf(entryDN), a.getName())); } else if (values.size() > 1) { throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, ERR_MODDN_PREOP_INCREMENT_MULTIPLE_VALUES.get( String.valueOf(entryDN), a.getName())); } long currentLongValue; try { AttributeValue v = values.iterator().next(); currentLongValue = Long.parseLong(v.getStringValue()); } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, ERR_MODDN_PREOP_INCREMENT_VALUE_NOT_INTEGER.get( String.valueOf(entryDN), a.getName())); } LinkedHashSet<AttributeValue> newValues = a.getValues(); if ((newValues == null) || newValues.isEmpty()) { throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, ERR_MODDN_PREOP_INCREMENT_NO_AMOUNT.get( String.valueOf(entryDN), a.getName())); } else if (newValues.size() > 1) { throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, ERR_MODDN_PREOP_INCREMENT_MULTIPLE_AMOUNTS.get( String.valueOf(entryDN), a.getName())); } long incrementAmount; try { AttributeValue v = values.iterator().next(); incrementAmount = Long.parseLong(v.getStringValue()); } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, ERR_MODDN_PREOP_INCREMENT_AMOUNT_NOT_INTEGER.get( String.valueOf(entryDN), a.getName())); } long newLongValue = currentLongValue + incrementAmount; ByteString newValueOS = new ASN1OctetString(String.valueOf(newLongValue)); newValues = new LinkedHashSet<AttributeValue>(1); newValues.add(new AttributeValue(a.getAttributeType(), newValueOS)); List<Attribute> newAttrList = new ArrayList<Attribute>(1); newAttrList.add(new Attribute(a.getAttributeType(), a.getName(), newValues)); newEntry.putAttribute(a.getAttributeType(), newAttrList); break; } } // Make sure that the updated entry still conforms to the server // schema. if (DirectoryServer.checkSchema()) { MessageBuilder invalidReason = new MessageBuilder(); if (! newEntry.conformsToSchema(null, false, true, true, invalidReason)) { throw new DirectoryException(ResultCode.OBJECTCLASS_VIOLATION, ERR_MODDN_PREOP_VIOLATES_SCHEMA.get( String.valueOf(entryDN), String.valueOf(invalidReason))); } } } /** * Performs any necessary processing to create the pre-read and/or post-read * response controls and attach them to the response. */ private void processReadEntryControls() { if (preReadRequest != null) { Entry entry = currentEntry.duplicate(true); if (! preReadRequest.allowsAttribute( DirectoryServer.getObjectClassAttributeType())) { entry.removeAttribute( DirectoryServer.getObjectClassAttributeType()); } if (! preReadRequest.returnAllUserAttributes()) { Iterator<AttributeType> iterator = entry.getUserAttributes().keySet().iterator(); while (iterator.hasNext()) { AttributeType attrType = iterator.next(); if (! preReadRequest.allowsAttribute(attrType)) { iterator.remove(); } } } if (! preReadRequest.returnAllOperationalAttributes()) { Iterator<AttributeType> iterator = entry.getOperationalAttributes().keySet().iterator(); while (iterator.hasNext()) { AttributeType attrType = iterator.next(); if (! preReadRequest.allowsAttribute(attrType)) { iterator.remove(); } } } // FIXME -- Check access controls on the entry to see if it should // be returned or if any attributes need to be stripped // out.. SearchResultEntry searchEntry = new SearchResultEntry(entry); LDAPPreReadResponseControl responseControl = new LDAPPreReadResponseControl(preReadRequest.getOID(), preReadRequest.isCritical(), searchEntry); addResponseControl(responseControl); } if (postReadRequest != null) { Entry entry = newEntry.duplicate(true); if (! postReadRequest.allowsAttribute( DirectoryServer.getObjectClassAttributeType())) { entry.removeAttribute( DirectoryServer.getObjectClassAttributeType()); } if (! postReadRequest.returnAllUserAttributes()) { Iterator<AttributeType> iterator = entry.getUserAttributes().keySet().iterator(); while (iterator.hasNext()) { AttributeType attrType = iterator.next(); if (! postReadRequest.allowsAttribute(attrType)) { iterator.remove(); } } } if (! postReadRequest.returnAllOperationalAttributes()) { Iterator<AttributeType> iterator = entry.getOperationalAttributes().keySet().iterator(); while (iterator.hasNext()) { AttributeType attrType = iterator.next(); if (! postReadRequest.allowsAttribute(attrType)) { iterator.remove(); } } } // FIXME -- Check access controls on the entry to see if it should // be returned or if any attributes need to be stripped // out.. SearchResultEntry searchEntry = new SearchResultEntry(entry); LDAPPostReadResponseControl responseControl = new LDAPPostReadResponseControl(postReadRequest.getOID(), postReadRequest.isCritical(), searchEntry); addResponseControl(responseControl); } } } opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendModifyOperation.java
@@ -26,41 +26,172 @@ */ package org.opends.server.workflowelement.localbackend; import java.util.List; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.concurrent.locks.Lock; import org.opends.messages.Message; import org.opends.messages.MessageBuilder; import org.opends.server.api.AttributeSyntax; import org.opends.server.api.Backend; import org.opends.server.api.ChangeNotificationListener; import org.opends.server.api.ClientConnection; import org.opends.server.api.PasswordStorageScheme; import org.opends.server.api.SynchronizationProvider; import org.opends.server.api.plugin.PostOperationPluginResult; import org.opends.server.api.plugin.PreOperationPluginResult; import org.opends.server.controls.LDAPAssertionRequestControl; 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.PasswordPolicyErrorType; import org.opends.server.controls.PasswordPolicyResponseControl; import org.opends.server.controls.ProxiedAuthV1Control; import org.opends.server.controls.ProxiedAuthV2Control; import org.opends.server.core.AccessControlConfigManager; import org.opends.server.core.DirectoryServer; import org.opends.server.core.ModifyOperation; import org.opends.server.core.ModifyOperationWrapper; import org.opends.server.core.PasswordPolicyState; import org.opends.server.core.PluginConfigManager; import org.opends.server.loggers.debug.DebugTracer; import org.opends.server.protocols.asn1.ASN1OctetString; import org.opends.server.schema.AuthPasswordSyntax; import org.opends.server.schema.BooleanSyntax; import org.opends.server.schema.UserPasswordSyntax; import org.opends.server.types.AccountStatusNotification; import org.opends.server.types.AccountStatusNotificationType; import org.opends.server.types.AcceptRejectWarn; import org.opends.server.types.Attribute; import org.opends.server.types.AttributeType; import org.opends.server.types.AttributeValue; import org.opends.server.types.AuthenticationInfo; import org.opends.server.types.ByteString; import org.opends.server.types.CancelledOperationException; import org.opends.server.types.CancelResult; import org.opends.server.types.Control; import org.opends.server.types.DebugLogLevel; import org.opends.server.types.DirectoryException; import org.opends.server.types.DN; import org.opends.server.types.Entry; import org.opends.server.types.LDAPException; import org.opends.server.types.LockManager; import org.opends.server.types.Modification; import org.opends.server.types.ModificationType; import org.opends.server.types.Privilege; import org.opends.server.types.RDN; import org.opends.server.types.ResultCode; import org.opends.server.types.SearchFilter; import org.opends.server.types.SearchResultEntry; import org.opends.server.types.SynchronizationProviderResult; import org.opends.server.types.operation.PostOperationModifyOperation; import org.opends.server.types.operation.PostResponseModifyOperation; import org.opends.server.types.operation.PreOperationModifyOperation; import org.opends.server.types.operation.PostSynchronizationModifyOperation; import static org.opends.messages.CoreMessages.*; 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.util.ServerConstants.*; import static org.opends.server.util.StaticUtils.*; /** * This class defines an operation used to modify an entry in a local backend * of the Directory Server. */ public class LocalBackendModifyOperation extends ModifyOperationWrapper implements PreOperationModifyOperation, PostOperationModifyOperation, PostResponseModifyOperation, PostSynchronizationModifyOperation public class LocalBackendModifyOperation extends ModifyOperationWrapper implements PreOperationModifyOperation, PostOperationModifyOperation, PostResponseModifyOperation, PostSynchronizationModifyOperation { /** * The tracer object for the debug logger. */ private static final DebugTracer TRACER = getTracer(); // The backend in which the target entry exists. private Backend backend; // Indicates whether the request included the user's current password. private boolean currentPasswordProvided; // Indicates whether the user's account has been enabled or disabled by this // modify operation. private boolean enabledStateChanged; // Indicates whether the user's account is currently enabled. private boolean isEnabled; // Indicates whether the request included the LDAP no-op control. private boolean noOp; // Indicates whether this modify operation includees a password change. private boolean passwordChanged; // Indicates whether the request included the password policy request control. private boolean pwPolicyControlRequested; // Indicates whether the password change is a self-change. private boolean selfChange; // Indicates whether to skip post-operation plugin processing. private boolean skipPostOperation; // Indicates whether the user's account was locked before this change. private boolean wasLocked; // The client connection associated with this operation. private ClientConnection clientConnection; // The DN of the entry to modify. private DN entryDN; // The current entry, before any changes are applied. private Entry currentEntry = null; // The modified entry that will be stored in the backend. private Entry modifiedEntry = null; // The number of passwords contained in the modify operation. private int numPasswords; // The post-read request control, if present. private LDAPPostReadRequestControl postReadRequest; // The pre-read request control, if present. private LDAPPreReadRequestControl preReadRequest; // The set of clear-text current passwords (if any were provided). private List<AttributeValue> currentPasswords = null; // The set of clear-text new passwords (if any were provided). private List<AttributeValue> newPasswords = null; // The set of modifications contained in this request. private List<Modification> modifications; // The password policy error type for this operation. private PasswordPolicyErrorType pwpErrorType; // The password policy state for this modify operation. private PasswordPolicyState pwPolicyState; /** * Creates a new operation that may be used to modify an entry in a * local backend of the Directory Server. @@ -73,6 +204,8 @@ LocalBackendWorkflowElement.attachLocalOperation (modify, this); } /** * Retrieves the current entry before any modifications are applied. This * will not be available to pre-parse plugins. @@ -85,6 +218,8 @@ return currentEntry; } /** * Retrieves the set of clear-text current passwords for the user, if * available. This will only be available if the modify operation contains @@ -101,6 +236,8 @@ return currentPasswords; } /** * Retrieves the modified entry that is to be written to the backend. This * will be available to pre-operation plugins, and if such a plugin does make @@ -115,6 +252,8 @@ return modifiedEntry; } /** * Retrieves the set of clear-text new passwords for the user, if available. * This will only be available if the modify operation contains one or more @@ -130,57 +269,6 @@ return newPasswords; } /** * Retrieves the current entry before any modifications are applied. This * will not be available to pre-parse plugins. * * @param currentEntry The current entry. */ public final void setCurrentEntry(Entry currentEntry) { this.currentEntry = currentEntry; } /** * Register the set of clear-text current passwords for the user, if * available. This will only be available if the modify operation contains * one or more delete elements that target the password attribute and provide * the values to delete in the clear. * * @param currentPasswords The set of clear-text current password values as * provided in the modify request. */ public final void setCurrentPasswords(List<AttributeValue> currentPasswords) { this.currentPasswords = currentPasswords; } /** * Register the modified entry that is to be written to the backend. * * @param modifiedEntry The modified entry that is to be written to the * backend, or <CODE>null</CODE> if it is not yet * available. */ public final void setModifiedEntry(Entry modifiedEntry) { this.modifiedEntry = modifiedEntry; } /** * Register the set of clear-text new passwords for the user, if available. * This will only be available if the modify operation contains one or more * add or replace elements that target the password attribute and provide the * values in the clear. * * @param newPasswords The set of clear-text new passwords as provided in * the modify request, or <CODE>null</CODE> if there * were none or this information is not yet available. */ public final void setNewPasswords(List<AttributeValue> newPasswords) { this.newPasswords = newPasswords; } /** @@ -202,4 +290,2123 @@ modifiedEntry.applyModification(modification); super.addModification(modification); } /** * Process this modify operation against a local backend. * * @param backend The backend in which the modify operation should be * performed. */ void processLocalModify(Backend backend) { this.backend = backend; clientConnection = getClientConnection(); // Get the plugin config manager that will be used for invoking plugins. PluginConfigManager pluginConfigManager = DirectoryServer.getPluginConfigManager(); skipPostOperation = false; // Create a labeled block of code that we can break out of if a problem is // detected. modifyProcessing: { entryDN = getEntryDN(); if (entryDN == null){ break modifyProcessing; } // Process the modifications to convert them from their raw form to the // form required for the rest of the modify processing. modifications = getModifications(); if (modifications == null) { break modifyProcessing; } if (modifications.isEmpty()) { setResultCode(ResultCode.CONSTRAINT_VIOLATION); appendErrorMessage(ERR_MODIFY_NO_MODIFICATIONS.get( String.valueOf(entryDN))); break modifyProcessing; } // If the user must change their password before doing anything else, and // if the target of the modify operation isn't the user's own entry, then // reject the request. if ((! isInternalOperation()) && clientConnection.mustChangePassword()) { DN authzDN = getAuthorizationDN(); if ((authzDN != null) && (! authzDN.equals(entryDN))) { // The user will not be allowed to do anything else before the // password gets changed. Also note that we haven't yet checked the // request controls so we need to do that now to see if the password // policy request control was provided. for (Control c : getRequestControls()) { if (c.getOID().equals(OID_PASSWORD_POLICY_CONTROL)) { pwPolicyControlRequested = true; pwpErrorType = PasswordPolicyErrorType.CHANGE_AFTER_RESET; break; } } setResultCode(ResultCode.UNWILLING_TO_PERFORM); appendErrorMessage(ERR_MODIFY_MUST_CHANGE_PASSWORD.get()); break modifyProcessing; } } // Check for a request to cancel this operation. if (getCancelRequest() != null) { return; } // Acquire a write lock on the target entry. Lock entryLock = null; for (int i=0; i < 3; i++) { entryLock = LockManager.lockWrite(entryDN); if (entryLock != null) { break; } } if (entryLock == null) { setResultCode(DirectoryServer.getServerErrorResultCode()); appendErrorMessage(ERR_MODIFY_CANNOT_LOCK_ENTRY.get( String.valueOf(entryDN))); skipPostOperation = true; break modifyProcessing; } try { // Check for a request to cancel this operation. if (getCancelRequest() != null) { return; } // Get the entry to modify. If it does not exist, then fail. try { currentEntry = backend.getEntry(entryDN); } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } setResponseData(de); break modifyProcessing; } if (currentEntry == null) { setResultCode(ResultCode.NO_SUCH_OBJECT); appendErrorMessage(ERR_MODIFY_NO_SUCH_ENTRY.get( String.valueOf(entryDN))); // See if one of the entry's ancestors exists. DN parentDN = entryDN.getParentDNInSuffix(); while (parentDN != null) { try { if (DirectoryServer.entryExists(parentDN)) { setMatchedDN(parentDN); break; } } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } break; } parentDN = parentDN.getParentDNInSuffix(); } break modifyProcessing; } // Check to see if there are any controls in the request. If so, then // see if there is any special processing required. try { processRequestControls(); } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } setResponseData(de); break modifyProcessing; } // Get the password policy state object for the entry that can be used // to perform any appropriate password policy processing. Also, see if // the entry is being updated by the end user or an administrator. selfChange = entryDN.equals(getAuthorizationDN()); try { // FIXME -- Need a way to enable debug mode. pwPolicyState = new PasswordPolicyState(currentEntry, false, false); } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } setResultCode(de.getResultCode()); appendErrorMessage(de.getMessageObject()); break modifyProcessing; } // Create a duplicate of the entry and apply the changes to it. modifiedEntry = currentEntry.duplicate(false); if (! noOp) { // Invoke any conflict resolution processing that might be needed by // the synchronization provider. for (SynchronizationProvider provider : DirectoryServer.getSynchronizationProviders()) { try { SynchronizationProviderResult result = provider.handleConflictResolution(this); if (! result.continueOperationProcessing()) { break modifyProcessing; } } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } logError(ERR_MODIFY_SYNCH_CONFLICT_RESOLUTION_FAILED.get( getConnectionID(), getOperationID(), getExceptionMessage(de))); setResponseData(de); break modifyProcessing; } } } try { handleInitialPasswordPolicyAndSchemaProcessing(); } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } setResponseData(de); break modifyProcessing; } // If there was a password change, then perform any additional checks // that may be necessary. wasLocked = false; if (passwordChanged) { try { performAdditionalPasswordChangedProcessing(); } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } setResponseData(de); break modifyProcessing; } } // Check to see if the client has permission to perform the modify. // The access control check is not made any earlier because the handler // needs access to the modified entry. // FIXME: for now assume that this will check all permissions // pertinent to the operation. This includes proxy authorization // and any other controls specified. // FIXME: earlier checks to see if the entry already exists may have // already exposed sensitive information to the client. if (! AccessControlConfigManager.getInstance(). getAccessControlHandler().isAllowed(this)) { setResultCode(ResultCode.INSUFFICIENT_ACCESS_RIGHTS); appendErrorMessage(ERR_MODIFY_AUTHZ_INSUFFICIENT_ACCESS_RIGHTS.get( String.valueOf(entryDN))); skipPostOperation = true; break modifyProcessing; } if ((! passwordChanged) && (! isInternalOperation()) && pwPolicyState.mustChangePassword()) { // The user will not be allowed to do anything else before the // password gets changed. pwpErrorType = PasswordPolicyErrorType.CHANGE_AFTER_RESET; setResultCode(ResultCode.UNWILLING_TO_PERFORM); appendErrorMessage(ERR_MODIFY_MUST_CHANGE_PASSWORD.get()); break modifyProcessing; } // If the server is configured to check the schema and the // operation is not a sycnhronization operation, // make sure that the new entry is valid per the server schema. if ((DirectoryServer.checkSchema()) && (! isSynchronizationOperation())) { MessageBuilder invalidReason = new MessageBuilder(); if (! modifiedEntry.conformsToSchema(null, false, false, false, invalidReason)) { setResultCode(ResultCode.OBJECTCLASS_VIOLATION); appendErrorMessage(ERR_MODIFY_VIOLATES_SCHEMA.get( String.valueOf(entryDN), invalidReason)); break modifyProcessing; } } // Check for a request to cancel this operation. if (getCancelRequest() != null) { return; } // If the operation is not a synchronization operation, // Invoke the pre-operation modify plugins. if (! isSynchronizationOperation()) { PreOperationPluginResult preOpResult = pluginConfigManager.invokePreOperationModifyPlugins(this); if (preOpResult.connectionTerminated()) { // There's no point in continuing with anything. Log the result // and return. setResultCode(ResultCode.CANCELED); appendErrorMessage(ERR_CANCELED_BY_PREOP_DISCONNECT.get()); setProcessingStopTime(); return; } else if (preOpResult.sendResponseImmediately()) { skipPostOperation = true; break modifyProcessing; } else if (preOpResult.skipCoreProcessing()) { skipPostOperation = false; break modifyProcessing; } } // Check for a request to cancel this operation. if (getCancelRequest() != null) { return; } // Actually perform the modify operation. This should also include // taking care of any synchronization that might be needed. if (backend == null) { setResultCode(ResultCode.NO_SUCH_OBJECT); appendErrorMessage(ERR_MODIFY_NO_BACKEND_FOR_ENTRY.get( String.valueOf(entryDN))); break modifyProcessing; } try { try { checkWritability(); } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } setResponseData(de); break modifyProcessing; } if (noOp) { appendErrorMessage(INFO_MODIFY_NOOP.get()); setResultCode(ResultCode.NO_OPERATION); } else { for (SynchronizationProvider provider : DirectoryServer.getSynchronizationProviders()) { try { SynchronizationProviderResult result = provider.doPreOperation(this); if (! result.continueOperationProcessing()) { break modifyProcessing; } } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } logError(ERR_MODIFY_SYNCH_PREOP_FAILED.get(getConnectionID(), getOperationID(), getExceptionMessage(de))); setResponseData(de); break modifyProcessing; } } backend.replaceEntry(modifiedEntry, this); // See if we need to generate any account status notifications as a // result of the changes. if (passwordChanged || enabledStateChanged || wasLocked) { handleAccountStatusNotifications(); } } // Handle any processing that may be needed for the pre-read and/or // post-read controls. handleReadEntryProcessing(); if (! noOp) { setResultCode(ResultCode.SUCCESS); } } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } setResponseData(de); break modifyProcessing; } catch (CancelledOperationException coe) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, coe); } CancelResult cancelResult = coe.getCancelResult(); setCancelResult(cancelResult); setResultCode(cancelResult.getResultCode()); Message message = coe.getMessageObject(); if ((message != null) && (message.length() > 0)) { appendErrorMessage(message); } break modifyProcessing; } } finally { LockManager.unlock(entryDN, entryLock); for (SynchronizationProvider provider : DirectoryServer.getSynchronizationProviders()) { try { provider.doPostOperation(this); } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } logError(ERR_MODIFY_SYNCH_POSTOP_FAILED.get(getConnectionID(), getOperationID(), getExceptionMessage(de))); setResponseData(de); break; } } } } // If the password policy request control was included, then make sure we // send the corresponding response control. if (pwPolicyControlRequested) { addResponseControl(new PasswordPolicyResponseControl(null, 0, pwpErrorType)); } // Indicate that it is now too late to attempt to cancel the operation. setCancelResult(CancelResult.TOO_LATE); // Invoke the post-operation or post-synchronization modify plugins. if (isSynchronizationOperation()) { if (getResultCode() == ResultCode.SUCCESS) { pluginConfigManager.invokePostSynchronizationModifyPlugins(this); } } else if (! skipPostOperation) { // FIXME -- Should this also be done while holding the locks? PostOperationPluginResult postOpResult = pluginConfigManager.invokePostOperationModifyPlugins(this); if (postOpResult.connectionTerminated()) { // There's no point in continuing with anything. Log the result and // return. setResultCode(ResultCode.CANCELED); appendErrorMessage(ERR_CANCELED_BY_PREOP_DISCONNECT.get()); setProcessingStopTime(); return; } } // Notify any change notification listeners that might be registered with // the server. if (getResultCode() == ResultCode.SUCCESS) { for (ChangeNotificationListener changeListener : DirectoryServer.getChangeNotificationListeners()) { try { changeListener.handleModifyOperation(this, currentEntry, modifiedEntry); } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } Message message = ERR_MODIFY_ERROR_NOTIFYING_CHANGE_LISTENER.get( getExceptionMessage(e)); logError(message); } } } // Stop the processing timer. setProcessingStopTime(); } /** * Processes any controls contained in the modify request. * * @throws DirectoryException If a problem is encountered with any of the * controls. */ private void processRequestControls() throws DirectoryException { List<Control> requestControls = getRequestControls(); if ((requestControls != null) && (! requestControls.isEmpty())) { for (int i=0; i < requestControls.size(); i++) { Control c = requestControls.get(i); String oid = c.getOID(); if (! AccessControlConfigManager.getInstance(). getAccessControlHandler().isAllowed(entryDN, this, c)) { skipPostOperation = true; throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS, ERR_CONTROL_INSUFFICIENT_ACCESS_RIGHTS.get(oid)); } if (oid.equals(OID_LDAP_ASSERTION)) { LDAPAssertionRequestControl assertControl; if (c instanceof LDAPAssertionRequestControl) { assertControl = (LDAPAssertionRequestControl) c; } else { try { assertControl = LDAPAssertionRequestControl.decodeControl(c); requestControls.set(i, assertControl); } catch (LDAPException le) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, le); } throw new DirectoryException( ResultCode.valueOf(le.getResultCode()), le.getMessageObject()); } } try { // FIXME -- We need to determine whether the current user has // permission to make this determination. SearchFilter filter = assertControl.getSearchFilter(); if (! filter.matchesEntry(currentEntry)) { throw new DirectoryException(ResultCode.ASSERTION_FAILED, ERR_MODIFY_ASSERTION_FAILED.get( String.valueOf(entryDN))); } } catch (DirectoryException de) { if (de.getResultCode() == ResultCode.ASSERTION_FAILED) { throw de; } if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } throw new DirectoryException(ResultCode.PROTOCOL_ERROR, ERR_MODIFY_CANNOT_PROCESS_ASSERTION_FILTER.get( String.valueOf(entryDN), de.getMessageObject())); } } else if (oid.equals(OID_LDAP_NOOP_OPENLDAP_ASSIGNED)) { noOp = true; } else if (oid.equals(OID_LDAP_READENTRY_PREREAD)) { if (c instanceof LDAPPreReadRequestControl) { preReadRequest = (LDAPPreReadRequestControl) c; } else { try { preReadRequest = LDAPPreReadRequestControl.decodeControl(c); requestControls.set(i, preReadRequest); } catch (LDAPException le) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, le); } throw new DirectoryException( ResultCode.valueOf(le.getResultCode()), le.getMessageObject()); } } } else if (oid.equals(OID_LDAP_READENTRY_POSTREAD)) { if (c instanceof LDAPPostReadRequestControl) { postReadRequest = (LDAPPostReadRequestControl) c; } else { try { postReadRequest = LDAPPostReadRequestControl.decodeControl(c); requestControls.set(i, postReadRequest); } catch (LDAPException le) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, le); } throw new DirectoryException( ResultCode.valueOf(le.getResultCode()), le.getMessageObject()); } } } else if (oid.equals(OID_PROXIED_AUTH_V1)) { // The requester must have the PROXIED_AUTH privilige in order to // be able to use this control. if (! clientConnection.hasPrivilege(Privilege.PROXIED_AUTH, this)) { throw new DirectoryException(ResultCode.AUTHORIZATION_DENIED, ERR_PROXYAUTH_INSUFFICIENT_PRIVILEGES.get()); } ProxiedAuthV1Control proxyControl; if (c instanceof ProxiedAuthV1Control) { proxyControl = (ProxiedAuthV1Control) c; } else { try { proxyControl = ProxiedAuthV1Control.decodeControl(c); } catch (LDAPException le) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, le); } throw new DirectoryException( ResultCode.valueOf(le.getResultCode()), le.getMessageObject()); } } Entry authorizationEntry = proxyControl.getAuthorizationEntry(); setAuthorizationEntry(authorizationEntry); if (authorizationEntry == null) { setProxiedAuthorizationDN(DN.nullDN()); } else { setProxiedAuthorizationDN(authorizationEntry.getDN()); } } else if (oid.equals(OID_PROXIED_AUTH_V2)) { // The requester must have the PROXIED_AUTH privilige in order to // be able to use this control. if (! clientConnection.hasPrivilege(Privilege.PROXIED_AUTH, this)) { throw new DirectoryException(ResultCode.AUTHORIZATION_DENIED, ERR_PROXYAUTH_INSUFFICIENT_PRIVILEGES.get()); } ProxiedAuthV2Control proxyControl; if (c instanceof ProxiedAuthV2Control) { proxyControl = (ProxiedAuthV2Control) c; } else { try { proxyControl = ProxiedAuthV2Control.decodeControl(c); } catch (LDAPException le) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, le); } throw new DirectoryException( ResultCode.valueOf(le.getResultCode()), le.getMessageObject()); } } Entry authorizationEntry = proxyControl.getAuthorizationEntry(); setAuthorizationEntry(authorizationEntry); if (authorizationEntry == null) { setProxiedAuthorizationDN(DN.nullDN()); } else { setProxiedAuthorizationDN(authorizationEntry.getDN()); } } else if (oid.equals(OID_PASSWORD_POLICY_CONTROL)) { pwPolicyControlRequested = true; } // NYI -- Add support for additional controls. else if (c.isCritical()) { if ((backend == null) || (! backend.supportsControl(oid))) { throw new DirectoryException( ResultCode.UNAVAILABLE_CRITICAL_EXTENSION, ERR_MODIFY_UNSUPPORTED_CRITICAL_CONTROL.get( String.valueOf(entryDN), oid)); } } } } } /** * Handles the initial set of password policy and schema processing for this * modify operation. * * @throws DirectoryException If a problem is encountered that should cause * the modify operation to fail. */ private void handleInitialPasswordPolicyAndSchemaProcessing() throws DirectoryException { // Declare variables used for password policy state processing. currentPasswordProvided = false; isEnabled = true; enabledStateChanged = false; if (currentEntry.hasAttribute( pwPolicyState.getPolicy().getPasswordAttribute())) { // It may actually have more than one, but we can't tell the difference if // the values are encoded, and its enough for our purposes just to know // that there is at least one. numPasswords = 1; } else { numPasswords = 0; } // If it's not an internal or synchronization operation, then iterate // through the set of modifications to see if a password is included in the // changes. If so, then add the appropriate state changes to the set of // modifications. if (! (isInternalOperation() || isSynchronizationOperation())) { for (Modification m : modifications) { if (m.getAttribute().getAttributeType().equals( pwPolicyState.getPolicy().getPasswordAttribute())) { passwordChanged = true; if (! selfChange) { if (! clientConnection.hasPrivilege(Privilege.PASSWORD_RESET, this)) { pwpErrorType = PasswordPolicyErrorType.PASSWORD_MOD_NOT_ALLOWED; throw new DirectoryException( ResultCode.INSUFFICIENT_ACCESS_RIGHTS, ERR_MODIFY_PWRESET_INSUFFICIENT_PRIVILEGES.get()); } } break; } } } for (Modification m : modifications) { Attribute a = m.getAttribute(); AttributeType t = a.getAttributeType(); // If the attribute type is marked "NO-USER-MODIFICATION" then fail unless // this is an internal operation or is related to synchronization in some // way. if (t.isNoUserModification()) { if (! (isInternalOperation() || isSynchronizationOperation() || m.isInternal())) { throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_MODIFY_ATTR_IS_NO_USER_MOD.get( String.valueOf(entryDN), a.getName())); } } // If the attribute type is marked "OBSOLETE" and the modification is // setting new values, then fail unless this is an internal operation or // is related to synchronization in some way. if (t.isObsolete()) { if (a.hasValue() && (m.getModificationType() != ModificationType.DELETE)) { if (! (isInternalOperation() || isSynchronizationOperation() || m.isInternal())) { throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_MODIFY_ATTR_IS_OBSOLETE.get( String.valueOf(entryDN), a.getName())); } } } // See if the attribute is one which controls the privileges available for // a user. If it is, then the client must have the PRIVILEGE_CHANGE // privilege. if (t.hasName(OP_ATTR_PRIVILEGE_NAME)) { if (! clientConnection.hasPrivilege(Privilege.PRIVILEGE_CHANGE, this)) { throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS, ERR_MODIFY_CHANGE_PRIVILEGE_INSUFFICIENT_PRIVILEGES.get()); } } // If the modification is updating the password attribute, then perform // any necessary password policy processing. This processing should be // skipped for synchronization operations. boolean isPassword = t.equals(pwPolicyState.getPolicy().getPasswordAttribute()); if (isPassword && (!(isSynchronizationOperation()))) { // If the attribute contains any options, then reject it. Passwords // will not be allowed to have options. Skipped for internal operations. if(! isInternalOperation()) { if (a.hasOptions()) { throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_MODIFY_PASSWORDS_CANNOT_HAVE_OPTIONS.get()); } // If it's a self change, then see if that's allowed. if (selfChange && (! pwPolicyState.getPolicy().allowUserPasswordChanges())) { pwpErrorType = PasswordPolicyErrorType.PASSWORD_MOD_NOT_ALLOWED; throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_MODIFY_NO_USER_PW_CHANGES.get()); } // If we require secure password changes, then makes sure it's a // secure communication channel. if (pwPolicyState.getPolicy().requireSecurePasswordChanges() && (! clientConnection.isSecure())) { pwpErrorType = PasswordPolicyErrorType.PASSWORD_MOD_NOT_ALLOWED; throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_MODIFY_REQUIRE_SECURE_CHANGES.get()); } // If it's a self change and it's not been long enough since the // previous change, then reject it. if (selfChange && pwPolicyState.isWithinMinimumAge()) { pwpErrorType = PasswordPolicyErrorType.PASSWORD_TOO_YOUNG; throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_MODIFY_WITHIN_MINIMUM_AGE.get()); } } // Check to see whether this will adding, deleting, or replacing // password values (increment doesn't make any sense for passwords). // Then perform the appropriate type of processing for that kind of // modification. boolean isAdd = (m.getModificationType() == ModificationType.ADD); LinkedHashSet<AttributeValue> pwValues = a.getValues(); LinkedHashSet<AttributeValue> encodedValues = new LinkedHashSet<AttributeValue>(); switch (m.getModificationType()) { case ADD: case REPLACE: processInitialAddOrReplacePW(isAdd, pwValues, encodedValues, a); break; case DELETE: processInitialDeletePW(pwValues, encodedValues, a); break; default: throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_MODIFY_INVALID_MOD_TYPE_FOR_PASSWORD.get( String.valueOf(m.getModificationType()), a.getName())); } } else { // See if it's an attribute used to maintain the account // enabled/disabled state. AttributeType disabledAttr = DirectoryServer.getAttributeType(OP_ATTR_ACCOUNT_DISABLED, true); if (t.equals(disabledAttr)) { enabledStateChanged = true; for (AttributeValue v : a.getValues()) { try { isEnabled = (! BooleanSyntax.decodeBooleanValue(v.getNormalizedValue())); } catch (DirectoryException de) { throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, ERR_MODIFY_INVALID_DISABLED_VALUE.get( OP_ATTR_ACCOUNT_DISABLED, String.valueOf(de.getMessageObject())), de); } } } } switch (m.getModificationType()) { case ADD: processInitialAddSchema(a); break; case DELETE: processInitialDeleteSchema(a); break; case REPLACE: processInitialReplaceSchema(a); break; case INCREMENT: processInitialIncrementSchema(a); break; } } } /** * Performs the initial password policy add or replace processing. * * @param isAdd Indicates whether it is an add or replace update. * @param pwValues The set of password values as included in the * request. * @param encodedValues The set of encoded password values. * @param pwAttr The attribute involved in the password change. * * @throws DirectoryException If a problem occurs that should cause the * modify operation to fail. */ private void processInitialAddOrReplacePW(boolean isAdd, LinkedHashSet<AttributeValue> pwValues, LinkedHashSet<AttributeValue> encodedValues, Attribute pwAttr) throws DirectoryException { int passwordsToAdd = pwValues.size(); if (isAdd) { numPasswords += passwordsToAdd; } else { numPasswords = passwordsToAdd; } // If there were multiple password values, then make sure that's OK. if ((! isInternalOperation()) && (! pwPolicyState.getPolicy().allowMultiplePasswordValues()) && (passwordsToAdd > 1)) { pwpErrorType = PasswordPolicyErrorType.PASSWORD_MOD_NOT_ALLOWED; throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_MODIFY_MULTIPLE_VALUES_NOT_ALLOWED.get()); } // Iterate through the password values and see if any of them are // pre-encoded. If so, then check to see if we'll allow it. Otherwise, // store the clear-text values for later validation and update the attribute // with the encoded values. for (AttributeValue v : pwValues) { if (pwPolicyState.passwordIsPreEncoded(v.getValue())) { if ((! isInternalOperation()) && ! pwPolicyState.getPolicy().allowPreEncodedPasswords()) { pwpErrorType = PasswordPolicyErrorType.INSUFFICIENT_PASSWORD_QUALITY; throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_MODIFY_NO_PREENCODED_PASSWORDS.get()); } else { encodedValues.add(v); } } else { if (isAdd) { // Make sure that the password value doesn't already exist. if (pwPolicyState.passwordMatches(v.getValue())) { pwpErrorType = PasswordPolicyErrorType.PASSWORD_IN_HISTORY; throw new DirectoryException(ResultCode.ATTRIBUTE_OR_VALUE_EXISTS, ERR_MODIFY_PASSWORD_EXISTS.get()); } } if (newPasswords == null) { newPasswords = new LinkedList<AttributeValue>(); } newPasswords.add(v); for (ByteString s : pwPolicyState.encodePassword(v.getValue())) { encodedValues.add(new AttributeValue(pwAttr.getAttributeType(), s)); } } } pwAttr.setValues(encodedValues); } /** * Performs the initial password policy delete processing. * * @param pwValues The set of password values as included in the * request. * @param encodedValues The set of encoded password values. * @param pwAttr The attribute involved in the password change. * * @throws DirectoryException If a problem occurs that should cause the * modify operation to fail. */ private void processInitialDeletePW(LinkedHashSet<AttributeValue> pwValues, LinkedHashSet<AttributeValue> encodedValues, Attribute pwAttr) throws DirectoryException { // Iterate through the password values and see if any of them are // pre-encoded. We will never allow pre-encoded passwords for user password // changes, but we will allow them for administrators. For each clear-text // value, verify that at least one value in the entry matches and replace // the clear-text value with the appropriate encoded forms. for (AttributeValue v : pwValues) { if (pwPolicyState.passwordIsPreEncoded(v.getValue())) { if ((! isInternalOperation()) && selfChange) { pwpErrorType = PasswordPolicyErrorType.INSUFFICIENT_PASSWORD_QUALITY; throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_MODIFY_NO_PREENCODED_PASSWORDS.get()); } else { encodedValues.add(v); } } else { List<Attribute> attrList = currentEntry.getAttribute(pwAttr.getAttributeType()); if ((attrList == null) || (attrList.isEmpty())) { throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_MODIFY_NO_EXISTING_VALUES.get()); } boolean found = false; for (Attribute attr : attrList) { for (AttributeValue av : attr.getValues()) { if (pwPolicyState.getPolicy().usesAuthPasswordSyntax()) { if (AuthPasswordSyntax.isEncoded(av.getValue())) { StringBuilder[] compoenents = AuthPasswordSyntax.decodeAuthPassword(av.getStringValue()); PasswordStorageScheme scheme = DirectoryServer.getAuthPasswordStorageScheme( compoenents[0].toString()); if (scheme != null) { if (scheme.authPasswordMatches(v.getValue(), compoenents[1].toString(), compoenents[2].toString())) { encodedValues.add(av); found = true; } } } else { if (av.equals(v)) { encodedValues.add(v); found = true; } } } else { if (UserPasswordSyntax.isEncoded(av.getValue())) { String[] compoenents = UserPasswordSyntax.decodeUserPassword( av.getStringValue()); PasswordStorageScheme scheme = DirectoryServer.getPasswordStorageScheme( toLowerCase(compoenents[0])); if (scheme != null) { if (scheme.passwordMatches(v.getValue(), new ASN1OctetString(compoenents[1]))) { encodedValues.add(av); found = true; } } } else { if (av.equals(v)) { encodedValues.add(v); found = true; } } } } } if (found) { if (currentPasswords == null) { currentPasswords = new LinkedList<AttributeValue>(); } currentPasswords.add(v); numPasswords--; } else { throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_MODIFY_INVALID_PASSWORD.get()); } currentPasswordProvided = true; } } pwAttr.setValues(encodedValues); } /** * Performs the initial schema processing for an add modification. * * @param attr The attribute being added. * * @throws DirectoryException If a problem occurs that should cause the * modify operation to fail. */ private void processInitialAddSchema(Attribute attr) throws DirectoryException { // Make sure that one or more values have been provided for the attribute. LinkedHashSet<AttributeValue> newValues = attr.getValues(); if ((newValues == null) || newValues.isEmpty()) { throw new DirectoryException(ResultCode.PROTOCOL_ERROR, ERR_MODIFY_ADD_NO_VALUES.get(String.valueOf(entryDN), attr.getName())); } // If the server is configured to check schema and the operation is not a // synchronization operation, make sure that all the new values are valid // according to the associated syntax. if ((DirectoryServer.checkSchema()) && (! isSynchronizationOperation())) { AcceptRejectWarn syntaxPolicy = DirectoryServer.getSyntaxEnforcementPolicy(); AttributeSyntax syntax = attr.getAttributeType().getSyntax(); if (syntaxPolicy == AcceptRejectWarn.REJECT) { MessageBuilder invalidReason = new MessageBuilder(); for (AttributeValue v : newValues) { if (! syntax.valueIsAcceptable(v.getValue(), invalidReason)) { throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, ERR_MODIFY_ADD_INVALID_SYNTAX.get( String.valueOf(entryDN), attr.getName(), v.getStringValue(), invalidReason)); } } } else if (syntaxPolicy == AcceptRejectWarn.WARN) { MessageBuilder invalidReason = new MessageBuilder(); for (AttributeValue v : newValues) { if (! syntax.valueIsAcceptable(v.getValue(), invalidReason)) { setResultCode(ResultCode.INVALID_ATTRIBUTE_SYNTAX); logError(ERR_MODIFY_ADD_INVALID_SYNTAX.get(String.valueOf(entryDN), attr.getName(), v.getStringValue(), invalidReason)); invalidReason = new MessageBuilder(); } } } } // Add the provided attribute or merge an existing attribute with // the values of the new attribute. If there are any duplicates, // then fail. if (attr.getAttributeType().isObjectClassType()) { modifiedEntry.addObjectClasses(newValues); } else { LinkedList<AttributeValue> duplicateValues = new LinkedList<AttributeValue>(); modifiedEntry.addAttribute(attr, duplicateValues); if (! duplicateValues.isEmpty()) { StringBuilder buffer = new StringBuilder(); Iterator<AttributeValue> iterator = duplicateValues.iterator(); buffer.append(iterator.next().getStringValue()); while (iterator.hasNext()) { buffer.append(", "); buffer.append(iterator.next().getStringValue()); } throw new DirectoryException(ResultCode.ATTRIBUTE_OR_VALUE_EXISTS, ERR_MODIFY_ADD_DUPLICATE_VALUE.get( String.valueOf(entryDN), attr.getName(), buffer)); } } } /** * Performs the initial schema processing for a delete modification. * * @param attr The attribute being deleted. * * @throws DirectoryException If a problem occurs that should cause the * modify operation to fail. */ private void processInitialDeleteSchema(Attribute attr) throws DirectoryException { // Remove the specified attribute values or the entire attribute from the // value. If there are any specified values that were not present, then // fail. If the RDN attribute value would be removed, then fail. LinkedList<AttributeValue> missingValues = new LinkedList<AttributeValue>(); boolean attrExists = modifiedEntry.removeAttribute(attr, missingValues); if (attrExists) { if (missingValues.isEmpty()) { AttributeType t = attr.getAttributeType(); RDN rdn = modifiedEntry.getDN().getRDN(); if ((rdn != null) && rdn.hasAttributeType(t) && (! modifiedEntry.hasValue(t, attr.getOptions(), rdn.getAttributeValue(t)))) { throw new DirectoryException(ResultCode.NOT_ALLOWED_ON_RDN, ERR_MODIFY_DELETE_RDN_ATTR.get( String.valueOf(entryDN), attr.getName())); } } else { StringBuilder buffer = new StringBuilder(); Iterator<AttributeValue> iterator = missingValues.iterator(); buffer.append(iterator.next().getStringValue()); while (iterator.hasNext()) { buffer.append(", "); buffer.append(iterator.next().getStringValue()); } throw new DirectoryException(ResultCode.NO_SUCH_ATTRIBUTE, ERR_MODIFY_DELETE_MISSING_VALUES.get( String.valueOf(entryDN), attr.getName(), buffer)); } } else { throw new DirectoryException(ResultCode.NO_SUCH_ATTRIBUTE, ERR_MODIFY_DELETE_NO_SUCH_ATTR.get( String.valueOf(entryDN), attr.getName())); } } /** * Performs the initial schema processing for a replace modification. * * @param attr The attribute being replaced. * * @throws DirectoryException If a problem occurs that should cause the * modify operation to fail. */ private void processInitialReplaceSchema(Attribute attr) throws DirectoryException { // If it is the objectclass attribute, then treat that separately. if (attr.getAttributeType().isObjectClassType()) { modifiedEntry.setObjectClasses(attr.getValues()); return; } // If the provided attribute does not have any values, then we will simply // remove the attribute from the entry (if it exists). AttributeType t = attr.getAttributeType(); if (! attr.hasValue()) { modifiedEntry.removeAttribute(t, attr.getOptions()); RDN rdn = modifiedEntry.getDN().getRDN(); if ((rdn != null) && rdn.hasAttributeType(t) && (! modifiedEntry.hasValue(t, attr.getOptions(), rdn.getAttributeValue(t)))) { throw new DirectoryException(ResultCode.NOT_ALLOWED_ON_RDN, ERR_MODIFY_DELETE_RDN_ATTR.get(String.valueOf(entryDN), attr.getName())); } return; } // If the server is configured to check schema and the operation is not a // synchronization operation, make sure that all the new values are valid // according to the associated syntax. LinkedHashSet<AttributeValue> newValues = attr.getValues(); if ((DirectoryServer.checkSchema()) && (! isSynchronizationOperation())) { AcceptRejectWarn syntaxPolicy = DirectoryServer.getSyntaxEnforcementPolicy(); AttributeSyntax syntax = t.getSyntax(); if (syntaxPolicy == AcceptRejectWarn.REJECT) { MessageBuilder invalidReason = new MessageBuilder(); for (AttributeValue v : newValues) { if (! syntax.valueIsAcceptable(v.getValue(), invalidReason)) { throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, ERR_MODIFY_REPLACE_INVALID_SYNTAX.get( String.valueOf(entryDN), attr.getName(), v.getStringValue(), invalidReason)); } } } else if (syntaxPolicy == AcceptRejectWarn.WARN) { MessageBuilder invalidReason = new MessageBuilder(); for (AttributeValue v : newValues) { if (! syntax.valueIsAcceptable(v.getValue(), invalidReason)) { setResultCode(ResultCode.INVALID_ATTRIBUTE_SYNTAX); logError(ERR_MODIFY_REPLACE_INVALID_SYNTAX.get( String.valueOf(entryDN), attr.getName(), v.getStringValue(), invalidReason)); invalidReason = new MessageBuilder(); } } } } // If the provided attribute does not have any options, then we will simply // use it in place of any existing attribute of the provided type (or add it // if it doesn't exist). if (! attr.hasOptions()) { List<Attribute> attrList = new ArrayList<Attribute>(1); attrList.add(attr); modifiedEntry.putAttribute(t, attrList); RDN rdn = modifiedEntry.getDN().getRDN(); if ((rdn != null) && rdn.hasAttributeType(t) && (! modifiedEntry.hasValue(t, attr.getOptions(), rdn.getAttributeValue(t)))) { throw new DirectoryException(ResultCode.NOT_ALLOWED_ON_RDN, ERR_MODIFY_DELETE_RDN_ATTR.get( String.valueOf(entryDN), attr.getName())); } return; } // See if there is an existing attribute of the provided type. If not, then // we'll use the new one. List<Attribute> attrList = modifiedEntry.getAttribute(t); if ((attrList == null) || attrList.isEmpty()) { attrList = new ArrayList<Attribute>(1); attrList.add(attr); modifiedEntry.putAttribute(t, attrList); RDN rdn = modifiedEntry.getDN().getRDN(); if ((rdn != null) && rdn.hasAttributeType(t) && (! modifiedEntry.hasValue(t, attr.getOptions(), rdn.getAttributeValue(t)))) { throw new DirectoryException(ResultCode.NOT_ALLOWED_ON_RDN, ERR_MODIFY_DELETE_RDN_ATTR.get( String.valueOf(entryDN), attr.getName())); } return; } // There must be an existing occurrence of the provided attribute in the // entry. If there is a version with exactly the set of options provided, // then replace it. Otherwise, add a new one. boolean found = false; for (int i=0; i < attrList.size(); i++) { if (attrList.get(i).optionsEqual(attr.getOptions())) { attrList.set(i, attr); found = true; break; } } if (! found) { attrList.add(attr); } RDN rdn = modifiedEntry.getDN().getRDN(); if ((rdn != null) && rdn.hasAttributeType(t) && (! modifiedEntry.hasValue(t, attr.getOptions(), rdn.getAttributeValue(t)))) { throw new DirectoryException(ResultCode.NOT_ALLOWED_ON_RDN, ERR_MODIFY_DELETE_RDN_ATTR.get( String.valueOf(entryDN), attr.getName())); } } /** * Performs the initial schema processing for an increment modification. * * @param attr The attribute being incremented. * * @throws DirectoryException If a problem occurs that should cause the * modify operation to fail. */ private void processInitialIncrementSchema(Attribute attr) throws DirectoryException { // The specified attribute type must not be an RDN attribute. AttributeType t = attr.getAttributeType(); RDN rdn = modifiedEntry.getDN().getRDN(); if ((rdn != null) && rdn.hasAttributeType(t)) { throw new DirectoryException(ResultCode.NOT_ALLOWED_ON_RDN, ERR_MODIFY_INCREMENT_RDN.get( String.valueOf(entryDN), attr.getName())); } // The provided attribute must have a single value, and it must be an // integer. LinkedHashSet<AttributeValue> values = attr.getValues(); if ((values == null) || values.isEmpty()) { throw new DirectoryException(ResultCode.PROTOCOL_ERROR, ERR_MODIFY_INCREMENT_REQUIRES_VALUE.get( String.valueOf(entryDN), attr.getName())); } else if (values.size() > 1) { throw new DirectoryException(ResultCode.PROTOCOL_ERROR, ERR_MODIFY_INCREMENT_REQUIRES_SINGLE_VALUE.get( String.valueOf(entryDN), attr.getName())); } AttributeValue v = values.iterator().next(); long incrementValue; try { incrementValue = Long.parseLong(v.getNormalizedStringValue()); } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, ERR_MODIFY_INCREMENT_PROVIDED_VALUE_NOT_INTEGER.get( String.valueOf(entryDN), attr.getName(), v.getStringValue()), e); } // Get the corresponding attribute from the entry and make sure that it has // a single integer value. List<Attribute> attrList = modifiedEntry.getAttribute(t, attr.getOptions()); if ((attrList == null) || attrList.isEmpty()) { throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, ERR_MODIFY_INCREMENT_REQUIRES_EXISTING_VALUE.get( String.valueOf(entryDN), attr.getName())); } boolean updated = false; for (Attribute a : attrList) { LinkedHashSet<AttributeValue> valueList = a.getValues(); if ((valueList == null) || valueList.isEmpty()) { continue; } LinkedHashSet<AttributeValue> newValueList = new LinkedHashSet<AttributeValue>(valueList.size()); for (AttributeValue existingValue : valueList) { long newIntValue; try { long existingIntValue = Long.parseLong(existingValue.getStringValue()); newIntValue = existingIntValue + incrementValue; } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, ERR_MODIFY_INCREMENT_REQUIRES_INTEGER_VALUE.get( String.valueOf(entryDN), a.getName(), existingValue.getStringValue()), e); } ByteString newValue = new ASN1OctetString(String.valueOf(newIntValue)); newValueList.add(new AttributeValue(t, newValue)); } a.setValues(newValueList); updated = true; } if (! updated) { throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, ERR_MODIFY_INCREMENT_REQUIRES_EXISTING_VALUE.get( String.valueOf(entryDN), attr.getName())); } } /** * Performs additional preliminary processing that is required for a password * change. * * @throws DirectoryException If a problem occurs that should cause the * modify operation to fail. */ public void performAdditionalPasswordChangedProcessing() throws DirectoryException { // If it was a self change, then see if the current password was provided // and handle accordingly. if (selfChange && pwPolicyState.getPolicy().requireCurrentPassword() && (! currentPasswordProvided)) { pwpErrorType = PasswordPolicyErrorType.MUST_SUPPLY_OLD_PASSWORD; throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_MODIFY_PW_CHANGE_REQUIRES_CURRENT_PW.get()); } // If this change would result in multiple password values, then see if // that's OK. if ((numPasswords > 1) && (! pwPolicyState.getPolicy().allowMultiplePasswordValues())) { pwpErrorType = PasswordPolicyErrorType.PASSWORD_MOD_NOT_ALLOWED; throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_MODIFY_MULTIPLE_PASSWORDS_NOT_ALLOWED.get()); } // If any of the password values should be validated, then do so now. if (selfChange || (! pwPolicyState.getPolicy().skipValidationForAdministrators())) { if (newPasswords != null) { HashSet<ByteString> clearPasswords = new HashSet<ByteString>(); clearPasswords.addAll(pwPolicyState.getClearPasswords()); if (currentPasswords != null) { if (clearPasswords.isEmpty()) { for (AttributeValue v : currentPasswords) { clearPasswords.add(v.getValue()); } } else { // NOTE: We can't rely on the fact that Set doesn't allow // duplicates because technically it's possible that the values // aren't duplicates if they are ASN.1 elements with different types // (like 0x04 for a standard universal octet string type versus 0x80 // for a simple password in a bind operation). So we have to // manually check for duplicates. for (AttributeValue v : currentPasswords) { ByteString pw = v.getValue(); boolean found = false; for (ByteString s : clearPasswords) { if (Arrays.equals(s.value(), pw.value())) { found = true; break; } } if (! found) { clearPasswords.add(pw); } } } } for (AttributeValue v : newPasswords) { MessageBuilder invalidReason = new MessageBuilder(); if (! pwPolicyState.passwordIsAcceptable(this, modifiedEntry, v.getValue(), clearPasswords, invalidReason)) { pwpErrorType = PasswordPolicyErrorType.INSUFFICIENT_PASSWORD_QUALITY; throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_MODIFY_PW_VALIDATION_FAILED.get( invalidReason)); } } } } // If we should check the password history, then do so now. if (pwPolicyState.maintainHistory()) { if (newPasswords != null) { for (AttributeValue v : newPasswords) { if (pwPolicyState.isPasswordInHistory(v.getValue())) { if (selfChange || (! pwPolicyState.getPolicy(). skipValidationForAdministrators())) { pwpErrorType = PasswordPolicyErrorType.PASSWORD_IN_HISTORY; throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_MODIFY_PW_IN_HISTORY.get()); } } } pwPolicyState.updatePasswordHistory(); } } // See if the account was locked for any reason. wasLocked = pwPolicyState.lockedDueToIdleInterval() || pwPolicyState.lockedDueToMaximumResetAge() || pwPolicyState.lockedDueToFailures(); // Update the password policy state attributes in the user's entry. If the // modification fails, then these changes won't be applied. pwPolicyState.setPasswordChangedTime(); pwPolicyState.clearFailureLockout(); pwPolicyState.clearGraceLoginTimes(); pwPolicyState.clearWarnedTime(); if (pwPolicyState.getPolicy().forceChangeOnAdd() || pwPolicyState.getPolicy().forceChangeOnReset()) { if (selfChange) { pwPolicyState.setMustChangePassword(false); } else { if ((pwpErrorType == null) && pwPolicyState.getPolicy().forceChangeOnReset()) { pwpErrorType = PasswordPolicyErrorType.CHANGE_AFTER_RESET; } pwPolicyState.setMustChangePassword( pwPolicyState.getPolicy().forceChangeOnReset()); } } if (pwPolicyState.getPolicy().getRequireChangeByTime() > 0) { pwPolicyState.setRequiredChangeTime(); } modifications.addAll(pwPolicyState.getModifications()); modifiedEntry.applyModifications(pwPolicyState.getModifications()); } /** * Checks to ensure that both the Directory Server and the backend are * writable. * * @throws DirectoryException If the modify operation should not be allowed * as a result of the writability check. */ private void checkWritability() throws DirectoryException { // If it is not a private backend, then check to see if the server or // backend is operating in read-only mode. if (! backend.isPrivateBackend()) { switch (DirectoryServer.getWritabilityMode()) { case DISABLED: throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_MODIFY_SERVER_READONLY.get( String.valueOf(entryDN))); case INTERNAL_ONLY: if (! (isInternalOperation() || isSynchronizationOperation())) { throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_MODIFY_SERVER_READONLY.get( String.valueOf(entryDN))); } } switch (backend.getWritabilityMode()) { case DISABLED: throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_MODIFY_BACKEND_READONLY.get( String.valueOf(entryDN))); case INTERNAL_ONLY: if (! isInternalOperation() || isSynchronizationOperation()) { throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_MODIFY_BACKEND_READONLY.get( String.valueOf(entryDN))); } } } } /** * Handles any account status notifications that may be needed as a result of * modify processing. */ private void handleAccountStatusNotifications() { if (passwordChanged) { if (selfChange) { AuthenticationInfo authInfo = clientConnection.getAuthenticationInfo(); if (authInfo.getAuthenticationDN().equals(modifiedEntry.getDN())) { clientConnection.setMustChangePassword(false); } Message message = INFO_MODIFY_PASSWORD_CHANGED.get(); pwPolicyState.generateAccountStatusNotification( AccountStatusNotificationType.PASSWORD_CHANGED, modifiedEntry, message, AccountStatusNotification.createProperties(pwPolicyState, false, -1, currentPasswords, newPasswords)); } else { Message message = INFO_MODIFY_PASSWORD_RESET.get(); pwPolicyState.generateAccountStatusNotification( AccountStatusNotificationType.PASSWORD_RESET, modifiedEntry, message, AccountStatusNotification.createProperties(pwPolicyState, false, -1, currentPasswords, newPasswords)); } } if (enabledStateChanged) { if (isEnabled) { Message message = INFO_MODIFY_ACCOUNT_ENABLED.get(); pwPolicyState.generateAccountStatusNotification( AccountStatusNotificationType.ACCOUNT_ENABLED, modifiedEntry, message, AccountStatusNotification.createProperties(pwPolicyState, false, -1, null, null)); } else { Message message = INFO_MODIFY_ACCOUNT_DISABLED.get(); pwPolicyState.generateAccountStatusNotification( AccountStatusNotificationType.ACCOUNT_DISABLED, modifiedEntry, message, AccountStatusNotification.createProperties(pwPolicyState, false, -1, null, null)); } } if (wasLocked) { Message message = INFO_MODIFY_ACCOUNT_UNLOCKED.get(); pwPolicyState.generateAccountStatusNotification( AccountStatusNotificationType.ACCOUNT_UNLOCKED, modifiedEntry, message, AccountStatusNotification.createProperties(pwPolicyState, false, -1, null, null)); } } /** * Handles any processing that is required for the LDAP pre-read and/or * post-read controls. */ private void handleReadEntryProcessing() { if (preReadRequest != null) { Entry entry = currentEntry.duplicate(true); if (! preReadRequest.allowsAttribute( DirectoryServer.getObjectClassAttributeType())) { entry.removeAttribute( DirectoryServer.getObjectClassAttributeType()); } if (! preReadRequest.returnAllUserAttributes()) { Iterator<AttributeType> iterator = entry.getUserAttributes().keySet().iterator(); while (iterator.hasNext()) { AttributeType attrType = iterator.next(); if (! preReadRequest.allowsAttribute(attrType)) { iterator.remove(); } } } if (! preReadRequest.returnAllOperationalAttributes()) { Iterator<AttributeType> iterator = entry.getOperationalAttributes().keySet().iterator(); while (iterator.hasNext()) { AttributeType attrType = iterator.next(); if (! preReadRequest.allowsAttribute(attrType)) { iterator.remove(); } } } // FIXME -- Check access controls on the entry to see if it should be // returned or if any attributes need to be stripped out.. SearchResultEntry searchEntry = new SearchResultEntry(entry); LDAPPreReadResponseControl responseControl = new LDAPPreReadResponseControl(preReadRequest.getOID(), preReadRequest.isCritical(), searchEntry); getResponseControls().add(responseControl); } if (postReadRequest != null) { Entry entry = modifiedEntry.duplicate(true); if (! postReadRequest.allowsAttribute( DirectoryServer.getObjectClassAttributeType())) { entry.removeAttribute( DirectoryServer.getObjectClassAttributeType()); } if (! postReadRequest.returnAllUserAttributes()) { Iterator<AttributeType> iterator = entry.getUserAttributes().keySet().iterator(); while (iterator.hasNext()) { AttributeType attrType = iterator.next(); if (! postReadRequest.allowsAttribute(attrType)) { iterator.remove(); } } } if (! postReadRequest.returnAllOperationalAttributes()) { Iterator<AttributeType> iterator = entry.getOperationalAttributes().keySet().iterator(); while (iterator.hasNext()) { AttributeType attrType = iterator.next(); if (! postReadRequest.allowsAttribute(attrType)) { iterator.remove(); } } } // FIXME -- Check access controls on the entry to see if it should be // returned or if any attributes need to be stripped out.. SearchResultEntry searchEntry = new SearchResultEntry(entry); LDAPPostReadResponseControl responseControl = new LDAPPostReadResponseControl(postReadRequest.getOID(), postReadRequest.isCritical(), searchEntry); getResponseControls().add(responseControl); } } } opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendSearchOperation.java
@@ -27,85 +27,621 @@ package org.opends.server.workflowelement.localbackend; import java.util.List; import org.opends.messages.Message; import org.opends.server.api.Backend; import org.opends.server.api.ClientConnection; import org.opends.server.api.plugin.PostOperationPluginResult; import org.opends.server.api.plugin.PreOperationPluginResult; import org.opends.server.controls.LDAPAssertionRequestControl; import org.opends.server.controls.MatchedValuesControl; import org.opends.server.controls.PersistentSearchControl; import org.opends.server.controls.ProxiedAuthV1Control; import org.opends.server.controls.ProxiedAuthV2Control; import org.opends.server.core.AccessControlConfigManager; import org.opends.server.core.DirectoryServer; import org.opends.server.core.PersistentSearch; import org.opends.server.core.PluginConfigManager; import org.opends.server.core.SearchOperationWrapper; import org.opends.server.core.SearchOperation; import org.opends.server.loggers.debug.DebugTracer; import org.opends.server.types.CancelResult; import org.opends.server.types.CancelledOperationException; import org.opends.server.types.Control; import org.opends.server.types.DebugLogLevel; import org.opends.server.types.DirectoryException; import org.opends.server.types.DN; import org.opends.server.types.Entry; import org.opends.server.types.LDAPException; import org.opends.server.types.Privilege; import org.opends.server.types.ResultCode; import org.opends.server.types.SearchFilter; import org.opends.server.types.operation.PostOperationSearchOperation; import org.opends.server.types.operation.PreOperationSearchOperation; import org.opends.server.types.operation.SearchEntrySearchOperation; import org.opends.server.types.operation.SearchReferenceSearchOperation; import static org.opends.messages.CoreMessages.*; import static org.opends.server.loggers.debug.DebugLogger.*; import static org.opends.server.util.ServerConstants.*; import static org.opends.server.util.StaticUtils.*; /** * This class defines an operation used to search for entries in a local backend * of the Directory Server. */ public class LocalBackendSearchOperation extends SearchOperationWrapper implements PreOperationSearchOperation, PostOperationSearchOperation, SearchEntrySearchOperation, SearchReferenceSearchOperation public class LocalBackendSearchOperation extends SearchOperationWrapper implements PreOperationSearchOperation, PostOperationSearchOperation, SearchEntrySearchOperation, SearchReferenceSearchOperation { /** * The tracer object for the debug logger. */ private static final DebugTracer TRACER = getTracer(); // The backend in which the search is to be performed. private Backend backend; // Indicates whether we should actually process the search. This should // only be false if it's a persistent search with changesOnly=true. private boolean processSearch; // Indicates whether to skip post-operation plugin processing. private boolean skipPostOperation; // The client connection for the search operation. private ClientConnection clientConnection; // The base DN for the search. private DN baseDN; // The persistent search request, if applicable. private PersistentSearch persistentSearch; // The filter for the search. private SearchFilter filter; /** * Creates a new operation that may be used to search for entries in a * local backend of the Directory Server. * Creates a new operation that may be used to search for entries in a local * backend of the Directory Server. * * @param search The operation to enhance. * @param search The operation to process. */ public LocalBackendSearchOperation(SearchOperation search){ public LocalBackendSearchOperation(SearchOperation search) { super(search); LocalBackendWorkflowElement.attachLocalOperation(search, this); } /** * Processes the search in the provided backend and recursively through its * subordinate backends. * Process this search operation against a local backend. * * @param backend The backend in which to process the search. * * @throws DirectoryException If a problem occurs while processing the * search. * * @throws CancelledOperationException If the backend noticed and reacted * to a request to cancel or abandon the * search operation. * @param backend The backend in which the search operation should be * performed. */ public final void searchBackend(Backend backend) throws DirectoryException, CancelledOperationException void processLocalSearch(Backend backend) { // Check for and handle a request to cancel this operation. this.backend = backend; clientConnection = getClientConnection(); // Get the plugin config manager that will be used for invoking plugins. PluginConfigManager pluginConfigManager = DirectoryServer.getPluginConfigManager(); skipPostOperation = false; processSearch = true; // Create a labeled block of code that we can break out of if a problem is // detected. searchProcessing: { // Process the search base and filter to convert them from their raw forms // as provided by the client to the forms required for the rest of the // search processing. baseDN = getBaseDN(); filter = getFilter(); if ((baseDN == null) || (filter == null)){ break searchProcessing; } // Check to see if there are any controls in the request. If so, then // see if there is any special processing required. try { handleRequestControls(); } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } setResponseData(de); break searchProcessing; } // Check to see if the client has permission to perform the // search. // FIXME: for now assume that this will check all permission // pertinent to the operation. This includes proxy authorization // and any other controls specified. if (! AccessControlConfigManager.getInstance().getAccessControlHandler(). isAllowed(this)) { setResultCode(ResultCode.INSUFFICIENT_ACCESS_RIGHTS); appendErrorMessage(ERR_SEARCH_AUTHZ_INSUFFICIENT_ACCESS_RIGHTS.get( String.valueOf(baseDN))); skipPostOperation = true; break searchProcessing; } // Check for a request to cancel this operation. if (getCancelRequest() != null) { return; } // Invoke the pre-operation search plugins. PreOperationPluginResult preOpResult = pluginConfigManager.invokePreOperationSearchPlugins(this); if (preOpResult.connectionTerminated()) { // There's no point in continuing with anything. Log the request and // result and return. setResultCode(ResultCode.CANCELED); appendErrorMessage(ERR_CANCELED_BY_PREOP_DISCONNECT.get()); setProcessingStopTime(); return; } else if (preOpResult.sendResponseImmediately()) { skipPostOperation = true; break searchProcessing; } else if (preOpResult.skipCoreProcessing()) { skipPostOperation = false; break searchProcessing; } // Check for a request to cancel this operation. if (getCancelRequest() != null) { return; } // Get the backend that should hold the search base. If there is none, // then fail. if (backend == null) { setResultCode(ResultCode.NO_SUCH_OBJECT); appendErrorMessage(ERR_SEARCH_BASE_DOESNT_EXIST.get( String.valueOf(baseDN))); break searchProcessing; } // We'll set the result code to "success". If a problem occurs, then it // will be overwritten. setResultCode(ResultCode.SUCCESS); // If there's a persistent search, then register it with the server. if (persistentSearch != null) { DirectoryServer.registerPersistentSearch(persistentSearch); setSendResponse(false); } // Process the search in the backend and all its subordinates. try { if (processSearch) { backend.search(this); } } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } setResponseData(de); if (persistentSearch != null) { DirectoryServer.deregisterPersistentSearch(persistentSearch); setSendResponse(true); } break searchProcessing; } catch (CancelledOperationException coe) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, coe); } CancelResult cancelResult = coe.getCancelResult(); setCancelResult(cancelResult); setResultCode(cancelResult.getResultCode()); Message message = coe.getMessageObject(); if ((message != null) && (message.length() > 0)) { appendErrorMessage(message); } if (persistentSearch != null) { DirectoryServer.deregisterPersistentSearch(persistentSearch); setSendResponse(true); } skipPostOperation = true; break searchProcessing; } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } setResultCode(DirectoryServer.getServerErrorResultCode()); appendErrorMessage(ERR_SEARCH_BACKEND_EXCEPTION.get( getExceptionMessage(e))); if (persistentSearch != null) { DirectoryServer.deregisterPersistentSearch(persistentSearch); setSendResponse(true); } skipPostOperation = true; break searchProcessing; } } // Check for a request to cancel this operation. if (getCancelRequest() != null) { setCancelResult(CancelResult.CANCELED); setProcessingStopTime(); return; } // Perform the search in the provided backend. backend.search(this); // Search in the subordinate backends is now done by the workflows. // Invoke the post-operation search plugins. if (! skipPostOperation) { PostOperationPluginResult postOperationResult = pluginConfigManager.invokePostOperationSearchPlugins(this); if (postOperationResult.connectionTerminated()) { setResultCode(ResultCode.CANCELED); appendErrorMessage(ERR_CANCELED_BY_POSTOP_DISCONNECT.get()); setProcessingStopTime(); return; } } } // If there are any subordinate backends, then process the search there as // well. // FIXME jdemendi - From now on, do not search in the subordinate backends // because this is done by the workflow topology. // Backend[] subBackends = backend.getSubordinateBackends(); // for (Backend b : subBackends) // { // DN[] baseDNs = b.getBaseDNs(); // for (DN dn : baseDNs) // { // if (dn.isDescendantOf(getBaseDN())) // { // searchBackend(b); // break; // } // } // } /** * Handles any controls contained in the request. * * @throws DirectoryException If there is a problem with any of the request * controls. */ private void handleRequestControls() throws DirectoryException { List<Control> requestControls = getRequestControls(); if ((requestControls != null) && (! requestControls.isEmpty())) { for (int i=0; i < requestControls.size(); i++) { Control c = requestControls.get(i); String oid = c.getOID(); if (! AccessControlConfigManager.getInstance(). getAccessControlHandler().isAllowed(baseDN, this, c)) { skipPostOperation = true; throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS, ERR_CONTROL_INSUFFICIENT_ACCESS_RIGHTS.get(oid)); } if (oid.equals(OID_LDAP_ASSERTION)) { LDAPAssertionRequestControl assertControl; if (c instanceof LDAPAssertionRequestControl) { assertControl = (LDAPAssertionRequestControl) c; } else { try { assertControl = LDAPAssertionRequestControl.decodeControl(c); requestControls.set(i, assertControl); } catch (LDAPException le) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, le); } throw new DirectoryException( ResultCode.valueOf(le.getResultCode()), le.getMessageObject(), le); } } try { // FIXME -- We need to determine whether the current user has // permission to make this determination. SearchFilter assertionFilter = assertControl.getSearchFilter(); Entry entry; try { entry = DirectoryServer.getEntry(baseDN); } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } throw new DirectoryException(de.getResultCode(), ERR_SEARCH_CANNOT_GET_ENTRY_FOR_ASSERTION.get( de.getMessageObject())); } if (entry == null) { throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, ERR_SEARCH_NO_SUCH_ENTRY_FOR_ASSERTION.get()); } if (! assertionFilter.matchesEntry(entry)) { throw new DirectoryException(ResultCode.ASSERTION_FAILED, ERR_SEARCH_ASSERTION_FAILED.get()); } } catch (DirectoryException de) { if (de.getResultCode() == ResultCode.ASSERTION_FAILED) { throw de; } if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } throw new DirectoryException(ResultCode.PROTOCOL_ERROR, ERR_SEARCH_CANNOT_PROCESS_ASSERTION_FILTER.get( de.getMessageObject()), de); } } else if (oid.equals(OID_PROXIED_AUTH_V1)) { // The requester must have the PROXIED_AUTH privilige in order to be // able to use this control. if (! clientConnection.hasPrivilege(Privilege.PROXIED_AUTH, this)) { throw new DirectoryException(ResultCode.AUTHORIZATION_DENIED, ERR_PROXYAUTH_INSUFFICIENT_PRIVILEGES.get()); } ProxiedAuthV1Control proxyControl; if (c instanceof ProxiedAuthV1Control) { proxyControl = (ProxiedAuthV1Control) c; } else { try { proxyControl = ProxiedAuthV1Control.decodeControl(c); } catch (LDAPException le) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, le); } throw new DirectoryException( ResultCode.valueOf(le.getResultCode()), le.getMessageObject(), le); } } Entry authorizationEntry = proxyControl.getAuthorizationEntry(); setAuthorizationEntry(authorizationEntry); if (authorizationEntry == null) { setProxiedAuthorizationDN(DN.nullDN()); } else { setProxiedAuthorizationDN(authorizationEntry.getDN()); } } else if (oid.equals(OID_PROXIED_AUTH_V2)) { // The requester must have the PROXIED_AUTH privilige in order to be // able to use this control. if (! clientConnection.hasPrivilege(Privilege.PROXIED_AUTH, this)) { throw new DirectoryException(ResultCode.AUTHORIZATION_DENIED, ERR_PROXYAUTH_INSUFFICIENT_PRIVILEGES.get()); } ProxiedAuthV2Control proxyControl; if (c instanceof ProxiedAuthV2Control) { proxyControl = (ProxiedAuthV2Control) c; } else { try { proxyControl = ProxiedAuthV2Control.decodeControl(c); } catch (LDAPException le) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, le); } throw new DirectoryException( ResultCode.valueOf(le.getResultCode()), le.getMessageObject(), le); } } Entry authorizationEntry = proxyControl.getAuthorizationEntry(); setAuthorizationEntry(authorizationEntry); if (authorizationEntry == null) { setProxiedAuthorizationDN(DN.nullDN()); } else { setProxiedAuthorizationDN(authorizationEntry.getDN()); } } else if (oid.equals(OID_PERSISTENT_SEARCH)) { PersistentSearchControl psearchControl; if (c instanceof PersistentSearchControl) { psearchControl = (PersistentSearchControl) c; } else { try { psearchControl = PersistentSearchControl.decodeControl(c); } catch (LDAPException le) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, le); } throw new DirectoryException( ResultCode.valueOf(le.getResultCode()), le.getMessageObject(), le); } } persistentSearch = new PersistentSearch(this, psearchControl.getChangeTypes(), psearchControl.getReturnECs()); setPersistentSearch(persistentSearch); // If we're only interested in changes, then we don't actually want // to process the search now. if (psearchControl.getChangesOnly()) { processSearch = false; } } else if (oid.equals(OID_LDAP_SUBENTRIES)) { setReturnLDAPSubentries(true); } else if (oid.equals(OID_MATCHED_VALUES)) { if (c instanceof MatchedValuesControl) { setMatchedValuesControl((MatchedValuesControl) c); } else { try { MatchedValuesControl matchedValuesControl = MatchedValuesControl.decodeControl(c); setMatchedValuesControl(matchedValuesControl); } catch (LDAPException le) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, le); } throw new DirectoryException( ResultCode.valueOf(le.getResultCode()), le.getMessageObject(), le); } } } else if (oid.equals(OID_ACCOUNT_USABLE_CONTROL)) { setIncludeUsableControl(true); } else if (oid.equals(OID_REAL_ATTRS_ONLY)) { setRealAttributesOnly(true); } else if (oid.equals(OID_VIRTUAL_ATTRS_ONLY)) { setVirtualAttributesOnly(true); } // NYI -- Add support for additional controls. else if (c.isCritical()) { if ((backend == null) || (! backend.supportsControl(oid))) { throw new DirectoryException( ResultCode.UNAVAILABLE_CRITICAL_EXTENSION, ERR_SEARCH_UNSUPPORTED_CRITICAL_CONTROL.get(oid)); } } } } } } opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendWorkflowElement.java
Diff too large