LocalBackendModifyOperation.java revision ff1293486c5e276c25f0c2c040ffda78520249c5
/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License, Version 1.0 only
* (the "License"). You may not use this file except in compliance
* with the License.
*
* You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at legal-notices/CDDLv1_0.txt.
* If applicable, add the following below this CDDL HEADER, with the
* fields enclosed by brackets "[]" replaced with your own identifying
* information:
* Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*
*
* Copyright 2008-2011 Sun Microsystems, Inc.
* Portions Copyright 2011-2015 ForgeRock AS
*/
/**
* 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,
{
/** The backend in which the target entry exists. */
/** 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 the request included the Permissive Modify control. */
private boolean permissiveModify;
/** Indicates whether this modify operation includes 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 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. */
/** The current entry, before any changes are applied. */
private Entry currentEntry;
/** The modified entry that will be stored in the backend. */
private Entry modifiedEntry;
/** The number of passwords contained in the modify operation. */
private int numPasswords;
/** The post-read request control, if present.*/
/** The pre-read request control, if present.*/
private LDAPPreReadRequestControl preReadRequest;
/** The set of clear-text current passwords (if any were provided).*/
/** The set of clear-text new passwords (if any were provided).*/
/** The set of modifications contained in this request. */
/** 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.
*
* @param modify The operation to enhance.
*/
{
super(modify);
}
/**
* Retrieves the current entry before any modifications are applied. This
* will not be available to pre-parse plugins.
*
* @return The current entry, or <CODE>null</CODE> if it is not yet
* available.
*/
public final Entry getCurrentEntry()
{
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
* one or more delete elements that target the password attribute and provide
* the values to delete in the clear. It will not be available to pre-parse
* plugins.
*
* @return The set of clear-text current password values as provided in the
* modify request, or <CODE>null</CODE> if there were none or this
* information is not yet available.
*/
{
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
* a change to this entry, then it is also necessary to add that change to
* the set of modifications to ensure that the update will be consistent.
*
* @return The modified entry that is to be written to the backend, or
* <CODE>null</CODE> if it is not yet available.
*/
public final Entry getModifiedEntry()
{
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
* add or replace elements that target the password attribute and provide the
* values in the clear. It will not be available to pre-parse plugins.
*
* @return 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.
*/
{
return newPasswords;
}
/**
* Adds the provided modification to the set of modifications to this modify
* operation.
* In addition, the modification is applied to the modified entry.
*
* This may only be called by pre-operation plugins.
*
* @param modification The modification to add to the set of changes for
* this modify operation.
*
* @throws DirectoryException If an unexpected problem occurs while applying
* the modification to the entry.
*/
throws DirectoryException
{
super.addModification(modification);
}
/**
* Process this modify operation against a local backend.
*
* @param wfe
* The local backend work-flow element.
* @throws CanceledOperationException
* if this operation should be cancelled
*/
throws CanceledOperationException
{
// Check for a request to cancel this operation.
checkIfCanceled(false);
try
{
// If the password policy request control was included, then make sure we
// send the corresponding response control.
{
}
// Invoke the post-operation or post-synchronization modify plugins.
if (isSynchronizationOperation())
{
{
}
}
else if (executePostOpPlugins.get())
{
// FIXME -- Should this also be done while holding the locks?
{
return;
}
}
}
finally
{
}
// Register a post-response call-back which will notify persistent
// searches and change listeners.
{
{
public void run()
{
{
}
}
});
}
}
throws CanceledOperationException
{
entryDN = getEntryDN();
{
return;
}
// Process the modifications to convert them from their raw form to the
// form required for the rest of the modify processing.
if (modifications == null)
{
return;
}
if (modifications.isEmpty())
{
return;
}
// Check for a request to cancel this operation.
checkIfCanceled(false);
// Acquire a write lock on the target entry.
try
{
{
return;
}
// Check for a request to cancel this operation.
checkIfCanceled(false);
// Get the entry to modify. If it does not exist, then fail.
if (currentEntry == null)
{
// See if one of the entry's ancestors exists.
return;
}
// Check to see if there are any controls in the request. If so, then
// see if there is any special processing required.
// 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.
// Check that the authorizing account is not required to change its password.
if (!isInternalOperation()
&& !selfChange
&& getAuthorizationEntry() != null)
{
if (authzPolicy.isPasswordPolicy())
{
if (authzState.mustChangePassword())
{
return;
}
}
}
// FIXME -- Need a way to enable debug mode.
if (policy.isPasswordPolicy())
{
}
// Create a duplicate of the entry and apply the changes to it.
if (!noOp && !handleConflictResolution())
{
return;
}
// 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.
try
{
if (!getAccessControlHandler().isAllowed(this))
{
return;
}
}
catch (DirectoryException e)
{
setResultCode(e.getResultCode());
return;
}
{
// The user did not attempt to change their password.
return;
}
if (mustCheckSchema())
{
// make sure that the new entry is valid per the server schema.
{
return;
}
}
// Check for a request to cancel this operation.
checkIfCanceled(false);
// If the operation is not a synchronization operation,
// Invoke the pre-operation modify plugins.
if (!isSynchronizationOperation())
{
executePostOpPlugins.set(true);
{
return;
}
}
// Actually perform the modify operation. This should also include
// taking care of any synchronization that might be needed.
{
return;
}
if (noOp)
{
}
else
{
if (!processPreOperation())
{
return;
}
// See if we need to generate any account status notifications as a
// result of the changes.
}
// post-read controls.
if (!noOp)
{
}
}
catch (DirectoryException de)
{
}
finally
{
{
}
}
}
private AccessControlHandler<?> getAccessControlHandler()
{
}
{
}
{
}
{
try
{
{
{
return matchedDN;
}
}
}
catch (Exception e)
{
logger.traceException(e);
}
return null;
}
/**
* 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
{
{
{
{
try
{
}
catch (DirectoryException de)
{
}
// Check if the current user has permission to make this determination.
{
throw new DirectoryException(
}
try
{
{
}
}
catch (DirectoryException de)
{
{
throw de;
}
}
}
{
noOp = true;
}
{
permissiveModify = true;
}
{
}
{
if (c instanceof LDAPPostReadRequestControl)
{
}
else
{
}
}
{
continue;
}
{
pwPolicyControlRequested = true;
}
// NYI -- Add support for additional controls.
else if (c.isCritical()
{
}
}
}
}
/**
* Handles schema processing for non-password modifications.
*
* @throws DirectoryException If a problem is encountered that should cause
* the modify operation to fail.
*/
private void handleSchemaProcessing() throws DirectoryException
{
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.
final boolean isInternalOrSynchro = isInternalOperation() || isSynchronizationOperation() || m.isInternal();
if (t.isNoUserModification() && !isInternalOrSynchro)
{
}
// 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()
&& !a.isEmpty()
&& !isInternalOrSynchro)
{
}
// 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 the modification is not updating the password attribute,
// then perform any schema processing.
if (!isPassword(t))
{
processInitialSchema(m.getModificationType(), a);
}
}
}
private boolean isPassword(AttributeType t)
{
return pwPolicyState != null
}
/**
* Handles the initial set of password policy for this modify operation.
*
* @throws DirectoryException If a problem is encountered that should cause
* the modify operation to fail.
*/
private void handleInitialPasswordPolicyProcessing() throws DirectoryException
{
// Declare variables used for password policy state processing.
currentPasswordProvided = false;
isEnabled = true;
enabledStateChanged = false;
if (pwPolicyState == null)
{
// Account not managed locally so nothing to do.
return;
}
{
// 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.
// FIXME, should this loop be merged with the next loop?
if (!isInternalOperation() && !isSynchronizationOperation())
{
for (Modification m : modifications)
{
if (isPassword(t))
{
passwordChanged = true;
{
throw new DirectoryException(
}
break;
}
}
}
for (Modification m : modifications)
{
Attribute a = m.getAttribute();
AttributeType t = a.getAttributeType();
// If the modification is updating the password attribute, then perform
// any necessary password policy processing. This processing should be
// skipped for synchronization operations.
if (isPassword(t))
{
if (!isSynchronizationOperation())
{
// If the attribute contains any options and new values are going to
// be added, then reject it. Passwords will not be allowed to have
// options. Skipped for internal operations.
if (!isInternalOperation())
{
if (a.hasOptions())
{
switch (m.getModificationType().asEnum())
{
case REPLACE:
if (!a.isEmpty())
{
}
// Allow delete operations to clean up after import.
break;
case ADD:
default:
// Allow delete operations to clean up after import.
break;
}
}
// If it's a self change, then see if that's allowed.
{
}
// If we require secure password changes, then makes sure it's a
// secure communication channel.
&& !clientConnection.isSecure())
{
}
// If it's a self change and it's not been long enough since the
// previous change, then reject it.
{
}
}
// 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.
switch (m.getModificationType().asEnum())
{
case ADD:
case REPLACE:
break;
case DELETE:
break;
default:
m.getModificationType(), a.getName()));
}
// Password processing may have changed the attribute in this modification.
a = m.getAttribute();
}
processInitialSchema(m.getModificationType(), a);
}
}
}
/**
* Performs the initial schema processing and updates the entry appropriately.
*
* @param modType
* The modification type to perform
* @param attr
* The attribute being operated on.
* @throws DirectoryException
* If a problem occurs that should cause the modify operation to fail.
*/
private void processInitialSchema(ModificationType modType, Attribute attr) throws DirectoryException
{
{
case ADD:
break;
case DELETE:
break;
case REPLACE:
break;
case INCREMENT:
break;
}
}
/**
* Performs the initial password policy add or replace processing.
*
* @param m
* The modification involved in the password change.
* @throws DirectoryException
* If a problem occurs that should cause the modify
* operation to fail.
*/
private void processInitialAddOrReplacePW(Modification m)
throws DirectoryException
{
{
}
else
{
}
// If there were multiple password values, then make sure that's OK.
if (!isInternalOperation()
&& passwordsToAdd > 1)
{
}
// 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 (ByteString v : pwAttr)
{
if (pwPolicyState.passwordIsPreEncoded(v))
{
if (!isInternalOperation()
{
}
}
else
{
// Make sure that the password value does not already exist.
&& pwPolicyState.passwordMatches(v))
{
}
if (newPasswords == null)
{
newPasswords = new LinkedList<>();
}
newPasswords.add(v);
}
}
}
/**
* Performs the initial password policy delete processing.
*
* @param m
* The modification involved in the password change.
* @throws DirectoryException
* If a problem occurs that should cause the modify
* operation to fail.
*/
{
// 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.
{
// Removing all current password values.
numPasswords = 0;
}
for (ByteString v : pwAttr)
{
if (pwPolicyState.passwordIsPreEncoded(v))
{
if (!isInternalOperation() && selfChange)
{
}
// We still need to check if the pre-encoded password matches
// an existing value, to decrease the number of passwords.
{
}
{
numPasswords--;
}
}
else
{
{
}
{
if (currentPasswords == null)
{
currentPasswords = new LinkedList<>();
}
currentPasswords.add(v);
numPasswords--;
}
else
{
}
currentPasswordProvided = true;
}
}
}
private boolean addIfAttributeValueExistsPreEncodedPassword(AttributeBuilder builder, List<Attribute> attrList,
{
{
{
{
return true;
}
}
}
return false;
}
private boolean addIfAttributeValueExistsNoPreEncodedPassword(AttributeBuilder builder, List<Attribute> attrList,
{
boolean found = false;
{
{
{
{
found = true;
}
}
{
found = true;
}
}
}
return found;
}
{
{
PasswordStorageScheme<?> scheme = DirectoryServer.getAuthPasswordStorageScheme(components[0].toString());
} else {
PasswordStorageScheme<?> scheme = DirectoryServer.getPasswordStorageScheme(toLowerCase(components[0]));
}
}
/**
* Performs the initial schema processing for an add modification
* and updates the entry appropriately.
*
* @param attr
* The attribute being added.
* @throws DirectoryException
* If a problem occurs that should cause the modify
* operation to fail.
*/
throws DirectoryException
{
// Make sure that one or more values have been provided for the attribute.
{
}
if (mustCheckSchema())
{
// make sure that all the new values are valid according to the associated syntax.
}
// If the attribute to be added is the object class attribute
// then make sure that all the object classes are known and not obsoleted.
{
}
// Add the provided attribute or merge an existing attribute with
// the values of the new attribute. If there are any duplicates, then fail.
{
}
}
private boolean mustCheckSchema()
{
}
/**
* Verifies that all the new values are valid according to the associated syntax.
*
* @throws DirectoryException
* If any of the new values violate the server schema configuration and server is
* configured to reject violations.
*/
{
for (ByteString v : attr)
{
{
switch (syntaxPolicy)
{
case REJECT:
case WARN:
// FIXME remove next line of code. According to Matt, since this is
// just a warning, the code should not set the resultCode
invalidReason = new LocalizableMessageBuilder();
break;
}
}
}
}
{
}
/**
* Ensures that the provided object class attribute contains known
* non-obsolete object classes.
*
* @param attr
* The object class attribute to validate.
* @throws DirectoryException
* If the attribute contained unknown or obsolete object
* classes.
*/
{
for (ByteString v : attr)
{
try
{
}
catch (Exception e)
{
logger.traceException(e);
}
{
}
if (oc.isObsolete())
{
}
}
}
/**
* Performs the initial schema processing for a delete modification
* and updates the entry appropriately.
*
* @param attr
* The attribute being deleted.
* @throws DirectoryException
* If a problem occurs that should cause the modify
* operation to fail.
*/
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.
if (attrExists)
{
if (missingValues.isEmpty())
{
&& rdn.hasAttributeType(t)
{
}
}
else if (!permissiveModify)
{
}
}
else if (!permissiveModify)
{
}
}
/**
* Performs the initial schema processing for a replace modification
* and updates the entry appropriately.
*
* @param attr
* The attribute being replaced.
* @throws DirectoryException
* If a problem occurs that should cause the modify
* operation to fail.
*/
throws DirectoryException
{
if (mustCheckSchema())
{
// make sure that all the new values are valid according to the associated syntax.
}
// If the attribute to be replaced is the object class attribute
// then make sure that all the object classes are known and not obsoleted.
{
}
// Replace the provided attribute.
// Make sure that the RDN attribute value(s) has not been removed.
&& rdn.hasAttributeType(t)
{
}
}
/**
* Performs the initial schema processing for an increment
* modification and updates the entry appropriately.
*
* @param attr
* The attribute being incremented.
* @throws DirectoryException
* If a problem occurs that should cause the modify
* operation to fail.
*/
throws DirectoryException
{
// The specified attribute type must not be an RDN attribute.
{
}
// The provided attribute must have a single value, and it must be an integer
{
}
{
}
long incrementValue;
try
{
}
catch (Exception e)
{
logger.traceException(e);
}
// Get the attribute that is to be incremented.
if (a == null)
{
}
// Increment each attribute value by the specified amount.
for (ByteString existingValue : a)
{
long currentValue;
try
{
}
catch (Exception e)
{
logger.traceException(e);
throw new DirectoryException(
e);
}
}
// Replace the existing attribute with the incremented version.
}
/**
* 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 (!passwordChanged
{
// Nothing to do.
return;
}
// If it was a self change, then see if the current password was provided
// and handle accordingly.
if (selfChange
{
}
// If this change would result in multiple password values, then see if that's OK.
{
}
// If any of the password values should be validated, then do so now.
if (newPasswords != null
{
if (currentPasswords != null)
{
}
for (ByteString v : newPasswords)
{
v, clearPasswords, invalidReason))
{
}
}
}
// If we should check the password history, then do so now.
{
for (ByteString v : newPasswords)
{
if (pwPolicyState.isPasswordInHistory(v)
{
}
}
}
// Update the password policy state attributes in the user's entry. If the
// modification fails, then these changes won't be applied.
{
if (selfChange)
{
pwPolicyState.setMustChangePassword(false);
}
else
{
{
}
}
}
{
}
}
/**
* Handles any account status notifications that may be needed as a result of
* modify processing.
*/
private void handleAccountStatusNotifications()
{
if (pwPolicyState == null)
{
// Account not managed locally, so nothing to do.
return;
}
{
// Account managed locally, but unchanged, so nothing to do.
return;
}
if (passwordChanged)
{
if (selfChange)
{
{
}
}
else
{
}
}
if (enabledStateChanged)
{
if (isEnabled)
{
}
else
{
}
}
if (wasLocked)
{
}
}
private void generateAccountStatusNotificationNoPwds(
{
}
private void generateAccountStatusNotificationForPwds(
{
AccountStatusNotification.createProperties(pwPolicyState, false, -1, currentPasswords, newPasswords));
}
/**
* Handle conflict resolution.
*
* @return {@code true} if processing should continue for the operation, or {@code false} if not.
*/
private boolean handleConflictResolution() {
try {
provider.handleConflictResolution(this);
if (! result.continueProcessing()) {
return false;
}
} catch (DirectoryException de) {
return false;
}
}
return true;
}
/**
* Process pre operation.
* @return {@code true} if processing should continue for the operation, or
* {@code false} if not.
*/
private boolean processPreOperation() {
try {
return false;
}
} catch (DirectoryException de) {
return false;
}
}
return true;
}
/** Invoke post operation synchronization providers. */
private void processSynchPostOperationPlugins() {
try {
provider.doPostOperation(this);
} catch (DirectoryException de) {
return;
}
}
}
}