001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2008-2011 Sun Microsystems, Inc.
015 * Portions Copyright 2011-2017 ForgeRock AS.
016 */
017package org.opends.server.workflowelement.localbackend;
018
019import java.math.BigInteger;
020import java.util.HashSet;
021import java.util.LinkedList;
022import java.util.List;
023import java.util.ListIterator;
024
025import org.forgerock.i18n.LocalizableMessage;
026import org.forgerock.i18n.LocalizableMessageBuilder;
027import org.forgerock.i18n.LocalizableMessageDescriptor.Arg3;
028import org.forgerock.i18n.LocalizableMessageDescriptor.Arg4;
029import org.forgerock.i18n.slf4j.LocalizedLogger;
030import org.forgerock.opendj.ldap.AttributeDescription;
031import org.forgerock.opendj.ldap.ByteString;
032import org.forgerock.opendj.ldap.DN;
033import org.forgerock.opendj.ldap.ModificationType;
034import org.forgerock.opendj.ldap.RDN;
035import org.forgerock.opendj.ldap.ResultCode;
036import org.forgerock.opendj.ldap.schema.AttributeType;
037import org.forgerock.opendj.ldap.schema.ObjectClass;
038import org.forgerock.opendj.ldap.schema.Syntax;
039import org.forgerock.util.Reject;
040import org.forgerock.util.Utils;
041import org.opends.server.api.AccessControlHandler;
042import org.opends.server.api.AuthenticationPolicy;
043import org.opends.server.api.Backend;
044import org.opends.server.api.ClientConnection;
045import org.opends.server.api.PasswordStorageScheme;
046import org.opends.server.api.SynchronizationProvider;
047import org.opends.server.api.plugin.PluginResult.PostOperation;
048import org.opends.server.controls.LDAPAssertionRequestControl;
049import org.opends.server.controls.LDAPPostReadRequestControl;
050import org.opends.server.controls.LDAPPreReadRequestControl;
051import org.opends.server.controls.PasswordPolicyErrorType;
052import org.opends.server.controls.PasswordPolicyResponseControl;
053import org.opends.server.core.AccessControlConfigManager;
054import org.opends.server.core.DirectoryServer;
055import org.opends.server.core.ModifyOperation;
056import org.opends.server.core.ModifyOperationWrapper;
057import org.opends.server.core.PasswordPolicy;
058import org.opends.server.core.PasswordPolicyState;
059import org.opends.server.core.PersistentSearch;
060import org.opends.server.schema.AuthPasswordSyntax;
061import org.opends.server.schema.UserPasswordSyntax;
062import org.opends.server.types.AcceptRejectWarn;
063import org.opends.server.types.AccountStatusNotification;
064import org.opends.server.types.AccountStatusNotificationType;
065import org.opends.server.types.Attribute;
066import org.opends.server.types.AttributeBuilder;
067import org.opends.server.types.AuthenticationInfo;
068import org.opends.server.types.CanceledOperationException;
069import org.opends.server.types.Control;
070import org.opends.server.types.DirectoryException;
071import org.opends.server.types.Entry;
072import org.opends.server.types.LockManager.DNLock;
073import org.opends.server.types.Modification;
074import org.opends.server.types.Privilege;
075import org.opends.server.types.SearchFilter;
076import org.opends.server.types.SynchronizationProviderResult;
077import org.opends.server.types.operation.PostOperationModifyOperation;
078import org.opends.server.types.operation.PostResponseModifyOperation;
079import org.opends.server.types.operation.PostSynchronizationModifyOperation;
080import org.opends.server.types.operation.PreOperationModifyOperation;
081
082import static org.opends.messages.CoreMessages.*;
083import static org.opends.server.config.ConfigConstants.*;
084import static org.opends.server.core.DirectoryServer.*;
085import static org.opends.server.types.AbstractOperation.*;
086import static org.opends.server.types.AccountStatusNotificationType.*;
087import static org.opends.server.util.ServerConstants.*;
088import static org.opends.server.util.StaticUtils.*;
089import static org.opends.server.workflowelement.localbackend.LocalBackendWorkflowElement.*;
090
091/** This class defines an operation used to modify an entry in a local backend of the Directory Server. */
092public class LocalBackendModifyOperation
093       extends ModifyOperationWrapper
094       implements PreOperationModifyOperation, PostOperationModifyOperation,
095                  PostResponseModifyOperation,
096                  PostSynchronizationModifyOperation
097{
098  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
099
100  /** The backend in which the target entry exists. */
101  private Backend<?> backend;
102  /** The client connection associated with this operation. */
103  private ClientConnection clientConnection;
104  private boolean preOperationPluginsExecuted;
105
106  /** Indicates whether this modify operation includes a password change. */
107  private boolean passwordChanged;
108  /** Indicates whether the password change is a self-change. */
109  private boolean selfChange;
110  /** Indicates whether the request included the user's current password. */
111  private boolean currentPasswordProvided;
112  /** Indicates whether the user's account has been enabled or disabled by this modify operation. */
113  private boolean enabledStateChanged;
114  /** Indicates whether the user's account is currently enabled. */
115  private boolean isEnabled;
116  /** Indicates whether the user's account was locked before this change. */
117  private boolean wasLocked;
118
119  /** Indicates whether the request included the LDAP no-op control. */
120  private boolean noOp;
121  /** Indicates whether the request included the Permissive Modify control. */
122  private boolean permissiveModify;
123  /** Indicates whether the request included the password policy request control. */
124  private boolean pwPolicyControlRequested;
125  /** The post-read request control, if present. */
126  private LDAPPostReadRequestControl postReadRequest;
127  /** The pre-read request control, if present. */
128  private LDAPPreReadRequestControl preReadRequest;
129
130  /** The DN of the entry to modify. */
131  private DN entryDN;
132  /** The current entry, before any changes are applied. */
133  private Entry currentEntry;
134  /** The modified entry that will be stored in the backend. */
135  private Entry modifiedEntry;
136  /** The set of modifications contained in this request. */
137  private List<Modification> modifications;
138
139  /** The number of passwords contained in the modify operation. */
140  private int numPasswords;
141
142  /** The set of clear-text current passwords (if any were provided). */
143  private List<ByteString> currentPasswords;
144  /** The set of clear-text new passwords (if any were provided). */
145  private List<ByteString> newPasswords;
146
147  /** The password policy error type for this operation. */
148  private PasswordPolicyErrorType pwpErrorType;
149  /** The password policy state for this modify operation. */
150  private PasswordPolicyState pwPolicyState;
151
152
153  /**
154   * Creates a new operation that may be used to modify an entry in a
155   * local backend of the Directory Server.
156   *
157   * @param modify The operation to enhance.
158   */
159  public LocalBackendModifyOperation(ModifyOperation modify)
160  {
161    super(modify);
162    LocalBackendWorkflowElement.attachLocalOperation (modify, this);
163  }
164
165  /**
166   * Returns whether authentication for this user is managed locally
167   * or via Pass-Through Authentication.
168   */
169  private boolean isAuthnManagedLocally()
170  {
171    return pwPolicyState != null;
172  }
173
174  /**
175   * Retrieves the current entry before any modifications are applied.  This
176   * will not be available to pre-parse plugins.
177   *
178   * @return  The current entry, or {@code null} if it is not yet available.
179   */
180  @Override
181  public final Entry getCurrentEntry()
182  {
183    return currentEntry;
184  }
185
186
187
188  /**
189   * Retrieves the set of clear-text current passwords for the user, if
190   * available.  This will only be available if the modify operation contains
191   * one or more delete elements that target the password attribute and provide
192   * the values to delete in the clear.  It will not be available to pre-parse
193   * plugins.
194   *
195   * @return  The set of clear-text current password values as provided in the
196   *          modify request, or {@code null} if there were none or this
197   *          information is not yet available.
198   */
199  @Override
200  public final List<ByteString> getCurrentPasswords()
201  {
202    return currentPasswords;
203  }
204
205
206
207  /**
208   * Retrieves the modified entry that is to be written to the backend.  This
209   * will be available to pre-operation plugins, and if such a plugin does make
210   * a change to this entry, then it is also necessary to add that change to
211   * the set of modifications to ensure that the update will be consistent.
212   *
213   * @return  The modified entry that is to be written to the backend, or
214   *          {@code null} if it is not yet available.
215   */
216  @Override
217  public final Entry getModifiedEntry()
218  {
219    return modifiedEntry;
220  }
221
222
223
224  /**
225   * Retrieves the set of clear-text new passwords for the user, if available.
226   * This will only be available if the modify operation contains one or more
227   * add or replace elements that target the password attribute and provide the
228   * values in the clear.  It will not be available to pre-parse plugins.
229   *
230   * @return  The set of clear-text new passwords as provided in the modify
231   *          request, or {@code null} if there were none or this
232   *          information is not yet available.
233   */
234  @Override
235  public final List<ByteString> getNewPasswords()
236  {
237    return newPasswords;
238  }
239
240
241
242  /**
243   * Adds the provided modification to the set of modifications to this modify operation.
244   * In addition, the modification is applied to the modified entry.
245   * <p>
246   * This may only be called by pre-operation plugins.
247   *
248   * @param  modification  The modification to add to the set of changes for
249   *                       this modify operation.
250   * @throws  DirectoryException  If an unexpected problem occurs while applying
251   *                              the modification to the entry.
252   */
253  @Override
254  public void addModification(Modification modification)
255    throws DirectoryException
256  {
257    modifiedEntry.applyModification(modification, permissiveModify);
258    super.addModification(modification);
259  }
260
261
262
263  /**
264   * Process this modify operation against a local backend.
265   *
266   * @param wfe
267   *          The local backend work-flow element.
268   * @throws CanceledOperationException
269   *           if this operation should be cancelled
270   */
271  void processLocalModify(final LocalBackendWorkflowElement wfe) throws CanceledOperationException
272  {
273    this.backend = wfe.getBackend();
274    this.clientConnection = getClientConnection();
275
276    checkIfCanceled(false);
277    try
278    {
279      processModify();
280
281      if (pwPolicyControlRequested)
282      {
283        addResponseControl(new PasswordPolicyResponseControl(null, 0, pwpErrorType));
284      }
285
286      invokePostModifyPlugins();
287    }
288    finally
289    {
290      LocalBackendWorkflowElement.filterNonDisclosableMatchedDN(this);
291    }
292
293
294    // Register a post-response call-back which will notify persistent
295    // searches and change listeners.
296    if (getResultCode() == ResultCode.SUCCESS)
297    {
298      registerPostResponseCallback(new Runnable()
299      {
300        @Override
301        public void run()
302        {
303          for (PersistentSearch psearch : backend.getPersistentSearches())
304          {
305            psearch.processModify(modifiedEntry, currentEntry);
306          }
307        }
308      });
309    }
310  }
311
312  private boolean invokePreModifyPlugins() throws CanceledOperationException
313  {
314    if (!isSynchronizationOperation())
315    {
316      preOperationPluginsExecuted = true;
317      if (!processOperationResult(this, getPluginConfigManager().invokePreOperationModifyPlugins(this)))
318      {
319        return false;
320      }
321    }
322    return true;
323  }
324
325  private void invokePostModifyPlugins()
326  {
327    if (isSynchronizationOperation())
328    {
329      if (getResultCode() == ResultCode.SUCCESS)
330      {
331        getPluginConfigManager().invokePostSynchronizationModifyPlugins(this);
332      }
333    }
334    else if (preOperationPluginsExecuted)
335    {
336      PostOperation result = getPluginConfigManager().invokePostOperationModifyPlugins(this);
337      if (!processOperationResult(this, result))
338      {
339        return;
340      }
341    }
342  }
343
344  private void processModify() throws CanceledOperationException
345  {
346    entryDN = getEntryDN();
347    if (entryDN == null)
348    {
349      return;
350    }
351    if (backend == null)
352    {
353      setResultCode(ResultCode.NO_SUCH_OBJECT);
354      appendErrorMessage(ERR_MODIFY_NO_BACKEND_FOR_ENTRY.get(entryDN));
355      return;
356    }
357
358    // Process the modifications to convert them from their raw form to the
359    // form required for the rest of the modify processing.
360    modifications = getModifications();
361    if (modifications == null)
362    {
363      return;
364    }
365
366    if (modifications.isEmpty())
367    {
368      setResultCode(ResultCode.CONSTRAINT_VIOLATION);
369      appendErrorMessage(ERR_MODIFY_NO_MODIFICATIONS.get(entryDN));
370      return;
371    }
372
373    checkIfCanceled(false);
374
375    // Acquire a write lock on the target entry.
376    final DNLock entryLock = DirectoryServer.getLockManager().tryWriteLockEntry(entryDN);
377    try
378    {
379      if (entryLock == null)
380      {
381        setResultCode(ResultCode.BUSY);
382        appendErrorMessage(ERR_MODIFY_CANNOT_LOCK_ENTRY.get(entryDN));
383        return;
384      }
385
386      checkIfCanceled(false);
387
388      currentEntry = backend.getEntry(entryDN);
389      if (currentEntry == null)
390      {
391        setResultCode(ResultCode.NO_SUCH_OBJECT);
392        appendErrorMessage(ERR_MODIFY_NO_SUCH_ENTRY.get(entryDN));
393        setMatchedDN(findMatchedDN(entryDN));
394        return;
395      }
396
397      processRequestControls();
398
399      // Get the password policy state object for the entry that can be used
400      // to perform any appropriate password policy processing. Also, see
401      // if the entry is being updated by the end user or an administrator.
402      final DN authzDN = getAuthorizationDN();
403      selfChange = entryDN.equals(authzDN);
404
405      // Should the authorizing account change its password?
406      if (mustChangePassword(selfChange, getAuthorizationEntry()))
407      {
408        pwpErrorType = PasswordPolicyErrorType.CHANGE_AFTER_RESET;
409        setResultCode(ResultCode.CONSTRAINT_VIOLATION);
410        appendErrorMessage(ERR_MODIFY_MUST_CHANGE_PASSWORD.get(authzDN != null ? authzDN : "anonymous"));
411        return;
412      }
413
414      // FIXME -- Need a way to enable debug mode.
415      pwPolicyState = createPasswordPolicyState(currentEntry);
416
417      // Create a duplicate of the entry and apply the changes to it.
418      modifiedEntry = currentEntry.duplicate(false);
419
420      if (!noOp && !handleConflictResolution())
421      {
422        return;
423      }
424
425      processNonPasswordModifications();
426
427      // Check to see if the client has permission to perform the modify.
428      // The access control check is not made any earlier because the handler
429      // needs access to the modified entry.
430
431      // FIXME: for now assume that this will check all permissions pertinent to the operation.
432      // This includes proxy authorization and any other controls specified.
433
434      // FIXME: earlier checks to see if the entry already exists may have
435      // already exposed sensitive information to the client.
436      if (!operationIsAllowed())
437      {
438        return;
439      }
440
441      if (isAuthnManagedLocally())
442      {
443        processPasswordPolicyModifications();
444        performAdditionalPasswordChangedProcessing();
445
446        if (currentUserMustChangePassword())
447        {
448          // The user did not attempt to change their password.
449          pwpErrorType = PasswordPolicyErrorType.CHANGE_AFTER_RESET;
450          setResultCode(ResultCode.CONSTRAINT_VIOLATION);
451          appendErrorMessage(ERR_MODIFY_MUST_CHANGE_PASSWORD.get(authzDN != null ? authzDN : "anonymous"));
452          return;
453        }
454      }
455
456      if (mustCheckSchema())
457      {
458        // make sure that the new entry is valid per the server schema.
459        LocalizableMessageBuilder invalidReason = new LocalizableMessageBuilder();
460        if (!modifiedEntry.conformsToSchema(null, false, false, false, invalidReason))
461        {
462          setResultCode(ResultCode.OBJECTCLASS_VIOLATION);
463          appendErrorMessage(ERR_MODIFY_VIOLATES_SCHEMA.get(entryDN, invalidReason));
464          return;
465        }
466      }
467
468      checkIfCanceled(false);
469
470      if (!invokePreModifyPlugins())
471      {
472        return;
473      }
474
475      // Actually perform the modify operation. This should also include
476      // taking care of any synchronization that might be needed.
477      LocalBackendWorkflowElement.checkIfBackendIsWritable(backend, this,
478          entryDN, ERR_MODIFY_SERVER_READONLY, ERR_MODIFY_BACKEND_READONLY);
479
480      if (noOp)
481      {
482        appendErrorMessage(INFO_MODIFY_NOOP.get());
483        setResultCode(ResultCode.NO_OPERATION);
484      }
485      else
486      {
487        if (!processPreOperation())
488        {
489          return;
490        }
491
492        backend.replaceEntry(currentEntry, modifiedEntry, this);
493
494        if (isAuthnManagedLocally())
495        {
496          generatePwpAccountStatusNotifications();
497        }
498      }
499
500      // Handle any processing that may be needed for the pre-read and/or post-read controls.
501      LocalBackendWorkflowElement.addPreReadResponse(this, preReadRequest, currentEntry);
502      LocalBackendWorkflowElement.addPostReadResponse(this, postReadRequest, modifiedEntry);
503
504      if (!noOp)
505      {
506        setResultCode(ResultCode.SUCCESS);
507      }
508    }
509    catch (DirectoryException de)
510    {
511      logger.traceException(de);
512
513      setResponseData(de);
514    }
515    finally
516    {
517      if (entryLock != null)
518      {
519        entryLock.unlock();
520      }
521      processSynchPostOperationPlugins();
522    }
523  }
524
525  private boolean operationIsAllowed()
526  {
527    try
528    {
529      if (!getAccessControlHandler().isAllowed(this))
530      {
531        setResultCodeAndMessageNoInfoDisclosure(modifiedEntry,
532            ResultCode.INSUFFICIENT_ACCESS_RIGHTS,
533            ERR_MODIFY_AUTHZ_INSUFFICIENT_ACCESS_RIGHTS.get(entryDN));
534        return false;
535      }
536      return true;
537    }
538    catch (DirectoryException e)
539    {
540      setResultCode(e.getResultCode());
541      appendErrorMessage(e.getMessageObject());
542      return false;
543    }
544  }
545
546  private boolean currentUserMustChangePassword()
547  {
548    return !isInternalOperation() && selfChange && !passwordChanged && pwPolicyState.mustChangePassword();
549  }
550
551  private boolean mustChangePassword(boolean selfChange, Entry authzEntry) throws DirectoryException
552  {
553    return !isInternalOperation() && !selfChange && authzEntry != null && mustChangePassword(authzEntry);
554  }
555
556  private boolean mustChangePassword(Entry authzEntry) throws DirectoryException
557  {
558    PasswordPolicyState authzState = createPasswordPolicyState(authzEntry);
559    return authzState != null && authzState.mustChangePassword();
560  }
561
562  private PasswordPolicyState createPasswordPolicyState(Entry entry) throws DirectoryException
563  {
564    AuthenticationPolicy policy = AuthenticationPolicy.forUser(entry, true);
565    if (policy.isPasswordPolicy())
566    {
567      return (PasswordPolicyState) policy.createAuthenticationPolicyState(entry);
568    }
569    return null;
570  }
571
572  private AccessControlHandler<?> getAccessControlHandler()
573  {
574    return AccessControlConfigManager.getInstance().getAccessControlHandler();
575  }
576
577  private DirectoryException newDirectoryException(Entry entry,
578      ResultCode resultCode, LocalizableMessage message) throws DirectoryException
579  {
580    return LocalBackendWorkflowElement.newDirectoryException(this, entry,
581        entryDN, resultCode, message, ResultCode.NO_SUCH_OBJECT,
582        ERR_MODIFY_NO_SUCH_ENTRY.get(entryDN));
583  }
584
585  private void setResultCodeAndMessageNoInfoDisclosure(Entry entry,
586      ResultCode realResultCode, LocalizableMessage realMessage) throws DirectoryException
587  {
588    LocalBackendWorkflowElement.setResultCodeAndMessageNoInfoDisclosure(this,
589        entry, entryDN, realResultCode, realMessage, ResultCode.NO_SUCH_OBJECT,
590        ERR_MODIFY_NO_SUCH_ENTRY.get(entryDN));
591  }
592
593  /**
594   * Processes any controls contained in the modify request.
595   *
596   * @throws  DirectoryException  If a problem is encountered with any of the
597   *                              controls.
598   */
599  private void processRequestControls() throws DirectoryException
600  {
601    LocalBackendWorkflowElement.evaluateProxyAuthControls(this);
602    LocalBackendWorkflowElement.removeAllDisallowedControls(entryDN, this);
603
604    for (ListIterator<Control> iter = getRequestControls().listIterator(); iter.hasNext();)
605    {
606      final Control c = iter.next();
607      final String oid = c.getOID();
608
609      if (OID_LDAP_ASSERTION.equals(oid))
610      {
611        LDAPAssertionRequestControl assertControl = getRequestControl(LDAPAssertionRequestControl.DECODER);
612
613        SearchFilter filter;
614        try
615        {
616          filter = assertControl.getSearchFilter();
617        }
618        catch (DirectoryException de)
619        {
620          logger.traceException(de);
621
622          throw newDirectoryException(currentEntry, de.getResultCode(),
623              ERR_MODIFY_CANNOT_PROCESS_ASSERTION_FILTER.get(entryDN, de.getMessageObject()));
624        }
625
626        // Check if the current user has permission to make this determination.
627        if (!getAccessControlHandler().isAllowed(this, currentEntry, filter))
628        {
629          throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS,
630              ERR_CONTROL_INSUFFICIENT_ACCESS_RIGHTS.get(oid));
631        }
632
633        try
634        {
635          if (!filter.matchesEntry(currentEntry))
636          {
637            throw newDirectoryException(currentEntry, ResultCode.ASSERTION_FAILED,
638                ERR_MODIFY_ASSERTION_FAILED.get(entryDN));
639          }
640        }
641        catch (DirectoryException de)
642        {
643          if (de.getResultCode() == ResultCode.ASSERTION_FAILED)
644          {
645            throw de;
646          }
647
648          logger.traceException(de);
649
650          throw newDirectoryException(currentEntry, de.getResultCode(),
651              ERR_MODIFY_CANNOT_PROCESS_ASSERTION_FILTER.get(entryDN, de.getMessageObject()));
652        }
653      }
654      else if (OID_LDAP_NOOP_OPENLDAP_ASSIGNED.equals(oid))
655      {
656        noOp = true;
657      }
658      else if (OID_PERMISSIVE_MODIFY_CONTROL.equals(oid))
659      {
660        permissiveModify = true;
661      }
662      else if (OID_LDAP_READENTRY_PREREAD.equals(oid))
663      {
664        preReadRequest = getRequestControl(LDAPPreReadRequestControl.DECODER);
665      }
666      else if (OID_LDAP_READENTRY_POSTREAD.equals(oid))
667      {
668        if (c instanceof LDAPPostReadRequestControl)
669        {
670          postReadRequest = (LDAPPostReadRequestControl) c;
671        }
672        else
673        {
674          postReadRequest = getRequestControl(LDAPPostReadRequestControl.DECODER);
675          iter.set(postReadRequest);
676        }
677      }
678      else if (LocalBackendWorkflowElement.isProxyAuthzControl(oid))
679      {
680        continue;
681      }
682      else if (OID_PASSWORD_POLICY_CONTROL.equals(oid))
683      {
684        pwPolicyControlRequested = true;
685      }
686      else if (c.isCritical() && !backend.supportsControl(oid))
687      {
688        throw newDirectoryException(currentEntry, ResultCode.UNAVAILABLE_CRITICAL_EXTENSION,
689            ERR_MODIFY_UNSUPPORTED_CRITICAL_CONTROL.get(entryDN, oid));
690      }
691    }
692  }
693
694  private void processNonPasswordModifications() throws DirectoryException
695  {
696    for (Modification m : modifications)
697    {
698      Attribute     a = m.getAttribute();
699      AttributeDescription attrDesc = a.getAttributeDescription();
700      AttributeType t = attrDesc.getAttributeType();
701
702
703      // If the attribute type is marked "NO-USER-MODIFICATION" then fail unless
704      // this is an internal operation or is related to synchronization in some way.
705      final boolean isInternalOrSynchro = isInternalOrSynchro(m);
706      if (t.isNoUserModification() && !isInternalOrSynchro)
707      {
708        throw newDirectoryException(currentEntry,
709            ResultCode.CONSTRAINT_VIOLATION,
710            ERR_MODIFY_ATTR_IS_NO_USER_MOD.get(entryDN, attrDesc));
711      }
712
713      // If the attribute type is marked "OBSOLETE" and the modification is
714      // setting new values, then fail unless this is an internal operation or
715      // is related to synchronization in some way.
716      if (t.isObsolete()
717          && !a.isEmpty()
718          && m.getModificationType() != ModificationType.DELETE
719          && !isInternalOrSynchro)
720      {
721        throw newDirectoryException(currentEntry,
722            ResultCode.CONSTRAINT_VIOLATION,
723            ERR_MODIFY_ATTR_IS_OBSOLETE.get(entryDN, attrDesc));
724      }
725
726
727      // See if the attribute is one which controls the privileges available for a user.
728      // If it is, then the client must have the PRIVILEGE_CHANGE privilege.
729      if (t.hasName(OP_ATTR_PRIVILEGE_NAME)
730          && !clientConnection.hasPrivilege(Privilege.PRIVILEGE_CHANGE, this))
731      {
732        throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS,
733                ERR_MODIFY_CHANGE_PRIVILEGE_INSUFFICIENT_PRIVILEGES.get());
734      }
735
736      // If the modification is not updating the password attribute,
737      // then perform any schema processing.
738      if (!isPassword(t))
739      {
740        processModification(m);
741      }
742    }
743  }
744
745  private boolean isInternalOrSynchro(Modification m)
746  {
747    return isInternalOperation() || m.isInternal() || isSynchronizationOperation();
748  }
749
750  private boolean isPassword(AttributeType t)
751  {
752    return pwPolicyState != null
753        && t.equals(pwPolicyState.getAuthenticationPolicy().getPasswordAttribute());
754  }
755
756  /** Processes the modifications related to password policy for this modify operation. */
757  private void processPasswordPolicyModifications() throws DirectoryException
758  {
759    // Declare variables used for password policy state processing.
760    currentPasswordProvided = false;
761    isEnabled = true;
762    enabledStateChanged = false;
763
764    final PasswordPolicy authPolicy = pwPolicyState.getAuthenticationPolicy();
765    if (currentEntry.hasAttribute(authPolicy.getPasswordAttribute()))
766    {
767      // It may actually have more than one, but we can't tell the difference if
768      // the values are encoded, and its enough for our purposes just to know
769      // that there is at least one.
770      numPasswords = 1;
771    }
772    else
773    {
774      numPasswords = 0;
775    }
776
777    passwordChanged = !isInternalOperation() && !isSynchronizationOperation() && isModifyingPassword();
778
779
780    for (Modification m : modifications)
781    {
782      AttributeType t = m.getAttribute().getAttributeDescription().getAttributeType();
783
784      // If the modification is updating the password attribute, then perform
785      // any necessary password policy processing.  This processing should be
786      // skipped for synchronization operations.
787      if (isPassword(t))
788      {
789        if (!isSynchronizationOperation())
790        {
791          // If the attribute contains any options and new values are going to
792          // be added, then reject it. Passwords will not be allowed to have options.
793          if (!isInternalOperation())
794          {
795            validatePasswordModification(m, authPolicy);
796          }
797          preProcessPasswordModification(m);
798        }
799
800        processModification(m);
801      }
802      else if (!isInternalOrSynchro(m)
803          && t.equals(getSchema().getAttributeType(OP_ATTR_ACCOUNT_DISABLED)))
804      {
805        enabledStateChanged = true;
806        isEnabled = !pwPolicyState.isDisabled();
807      }
808    }
809  }
810
811  /** Adds the appropriate state changes for the provided modification. */
812  private void preProcessPasswordModification(Modification m) throws DirectoryException
813  {
814    switch (m.getModificationType().asEnum())
815    {
816    case ADD:
817    case REPLACE:
818      preProcessPasswordAddOrReplace(m);
819      break;
820
821    case DELETE:
822      preProcessPasswordDelete(m);
823      break;
824
825    // case INCREMENT does not make any sense for passwords
826    default:
827      AttributeDescription attrDesc = m.getAttribute().getAttributeDescription();
828      throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION,
829          ERR_MODIFY_INVALID_MOD_TYPE_FOR_PASSWORD.get(m.getModificationType(), attrDesc));
830    }
831  }
832
833  private boolean isModifyingPassword() throws DirectoryException
834  {
835    for (Modification m : modifications)
836    {
837      if (isPassword(m.getAttribute().getAttributeDescription().getAttributeType()))
838      {
839        if (!selfChange && !clientConnection.hasPrivilege(Privilege.PASSWORD_RESET, this))
840        {
841          pwpErrorType = PasswordPolicyErrorType.PASSWORD_MOD_NOT_ALLOWED;
842          throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS,
843              ERR_MODIFY_PWRESET_INSUFFICIENT_PRIVILEGES.get());
844        }
845        return true;
846      }
847    }
848    return false;
849  }
850
851  private void validatePasswordModification(Modification m, PasswordPolicy authPolicy) throws DirectoryException
852  {
853    Attribute a = m.getAttribute();
854    if (a.getAttributeDescription().hasOptions())
855    {
856      switch (m.getModificationType().asEnum())
857      {
858      case REPLACE:
859        if (!a.isEmpty())
860        {
861          throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION,
862              ERR_MODIFY_PASSWORDS_CANNOT_HAVE_OPTIONS.get());
863        }
864        // Allow delete operations to clean up after import.
865        break;
866      case ADD:
867        throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION,
868            ERR_MODIFY_PASSWORDS_CANNOT_HAVE_OPTIONS.get());
869      default:
870        // Allow delete operations to clean up after import.
871        break;
872      }
873    }
874
875    // If it's a self change, then see if that's allowed.
876    if (selfChange && !authPolicy.isAllowUserPasswordChanges())
877    {
878      pwpErrorType = PasswordPolicyErrorType.PASSWORD_MOD_NOT_ALLOWED;
879      throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
880          ERR_MODIFY_NO_USER_PW_CHANGES.get());
881    }
882
883
884    // If we require secure password changes, then makes sure it's a
885    // secure communication channel.
886    if (authPolicy.isRequireSecurePasswordChanges()
887        && !clientConnection.isSecure())
888    {
889      pwpErrorType = PasswordPolicyErrorType.PASSWORD_MOD_NOT_ALLOWED;
890      throw new DirectoryException(ResultCode.CONFIDENTIALITY_REQUIRED,
891          ERR_MODIFY_REQUIRE_SECURE_CHANGES.get());
892    }
893
894
895    // If it's a self change and it's not been long enough since the
896    // previous change, then reject it.
897    if (selfChange && pwPolicyState.isWithinMinimumAge())
898    {
899      pwpErrorType = PasswordPolicyErrorType.PASSWORD_TOO_YOUNG;
900      throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
901          ERR_MODIFY_WITHIN_MINIMUM_AGE.get());
902    }
903  }
904
905  /**
906   * Process the provided modification and updates the entry appropriately.
907   *
908   * @param m
909   *          The modification to perform
910   * @throws DirectoryException
911   *           If a problem occurs that should cause the modify operation to fail.
912   */
913  private void processModification(Modification m) throws DirectoryException
914  {
915    Attribute attr = m.getAttribute();
916    switch (m.getModificationType().asEnum())
917    {
918    case ADD:
919      processAddModification(attr);
920      break;
921
922    case DELETE:
923      processDeleteModification(attr);
924      break;
925
926    case REPLACE:
927      processReplaceModification(attr);
928      break;
929
930    case INCREMENT:
931      processIncrementModification(attr);
932      break;
933    }
934  }
935
936  private void preProcessPasswordAddOrReplace(Modification m) throws DirectoryException
937  {
938    Attribute pwAttr = m.getAttribute();
939    int passwordsToAdd = pwAttr.size();
940
941    if (m.getModificationType() == ModificationType.ADD)
942    {
943      numPasswords += passwordsToAdd;
944    }
945    else
946    {
947      numPasswords = passwordsToAdd;
948    }
949
950    // If there were multiple password values, then make sure that's OK.
951    final PasswordPolicy authPolicy = pwPolicyState.getAuthenticationPolicy();
952    if (!isInternalOperation()
953        && !authPolicy.isAllowMultiplePasswordValues()
954        && passwordsToAdd > 1)
955    {
956      pwpErrorType = PasswordPolicyErrorType.PASSWORD_MOD_NOT_ALLOWED;
957      throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION,
958          ERR_MODIFY_MULTIPLE_VALUES_NOT_ALLOWED.get());
959    }
960
961    // Iterate through the password values and see if any of them are
962    // pre-encoded. If so, then check to see if we'll allow it.
963    // Otherwise, store the clear-text values for later validation
964    // and update the attribute with the encoded values.
965    AttributeBuilder builder = new AttributeBuilder(pwAttr.getAttributeDescription());
966    for (ByteString v : pwAttr)
967    {
968      if (pwPolicyState.passwordIsPreEncoded(v))
969      {
970        if (!isInternalOperation()
971            && !authPolicy.isAllowPreEncodedPasswords())
972        {
973          pwpErrorType = PasswordPolicyErrorType.INSUFFICIENT_PASSWORD_QUALITY;
974          throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION,
975              ERR_MODIFY_NO_PREENCODED_PASSWORDS.get());
976        }
977
978        builder.add(v);
979      }
980      else
981      {
982        if (m.getModificationType() == ModificationType.ADD
983            // Make sure that the password value does not already exist.
984            && pwPolicyState.passwordMatches(v))
985        {
986          pwpErrorType = PasswordPolicyErrorType.PASSWORD_IN_HISTORY;
987          throw new DirectoryException(ResultCode.ATTRIBUTE_OR_VALUE_EXISTS,
988              ERR_MODIFY_PASSWORD_EXISTS.get());
989        }
990
991        if (newPasswords == null)
992        {
993          newPasswords = new LinkedList<>();
994        }
995        newPasswords.add(v);
996
997        builder.addAll(pwPolicyState.encodePassword(v));
998      }
999    }
1000
1001    m.setAttribute(builder.toAttribute());
1002  }
1003
1004  private void preProcessPasswordDelete(Modification m) throws DirectoryException
1005  {
1006    // Iterate through the password values and see if any of them are pre-encoded.
1007    // We will never allow pre-encoded passwords for user password changes,
1008    // but we will allow them for administrators.
1009    // For each clear-text value, verify that at least one value in the entry matches
1010    // and replace the clear-text value with the appropriate encoded forms.
1011    Attribute pwAttr = m.getAttribute();
1012    if (pwAttr.isEmpty())
1013    {
1014      // Removing all current password values.
1015      numPasswords = 0;
1016    }
1017
1018    AttributeDescription pwdAttrDesc = pwAttr.getAttributeDescription();
1019    AttributeBuilder builder = new AttributeBuilder(pwdAttrDesc);
1020    for (ByteString v : pwAttr)
1021    {
1022      if (pwPolicyState.passwordIsPreEncoded(v))
1023      {
1024        if (!isInternalOperation() && selfChange)
1025        {
1026          pwpErrorType = PasswordPolicyErrorType.INSUFFICIENT_PASSWORD_QUALITY;
1027          throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION,
1028              ERR_MODIFY_NO_PREENCODED_PASSWORDS.get());
1029        }
1030
1031        // We still need to check if the pre-encoded password matches
1032        // an existing value, to decrease the number of passwords.
1033        List<Attribute> attrList = currentEntry.getAttribute(pwdAttrDesc.getAttributeType());
1034        if (attrList.isEmpty())
1035        {
1036          throw new DirectoryException(ResultCode.NO_SUCH_ATTRIBUTE, ERR_MODIFY_NO_EXISTING_VALUES.get());
1037        }
1038
1039        if (addIfAttributeValueExistsPreEncodedPassword(builder, attrList, v))
1040        {
1041          numPasswords--;
1042        }
1043      }
1044      else
1045      {
1046        List<Attribute> attrList = currentEntry.getAttribute(pwdAttrDesc.getAttributeType());
1047        if (attrList.isEmpty())
1048        {
1049          throw new DirectoryException(ResultCode.NO_SUCH_ATTRIBUTE, ERR_MODIFY_NO_EXISTING_VALUES.get());
1050        }
1051
1052        if (addIfAttributeValueExistsNoPreEncodedPassword(builder, attrList, v))
1053        {
1054          if (currentPasswords == null)
1055          {
1056            currentPasswords = new LinkedList<>();
1057          }
1058          currentPasswords.add(v);
1059          numPasswords--;
1060        }
1061        else
1062        {
1063          throw new DirectoryException(ResultCode.NO_SUCH_ATTRIBUTE,
1064              ERR_MODIFY_INVALID_PASSWORD.get());
1065        }
1066
1067        currentPasswordProvided = true;
1068      }
1069    }
1070
1071    m.setAttribute(builder.toAttribute());
1072  }
1073
1074  private boolean addIfAttributeValueExistsPreEncodedPassword(AttributeBuilder builder, List<Attribute> attrList,
1075      ByteString val)
1076  {
1077    for (Attribute attr : attrList)
1078    {
1079      for (ByteString av : attr)
1080      {
1081        if (av.equals(val))
1082        {
1083          builder.add(val);
1084          return true;
1085        }
1086      }
1087    }
1088    return false;
1089  }
1090
1091  private boolean addIfAttributeValueExistsNoPreEncodedPassword(AttributeBuilder builder, List<Attribute> attrList,
1092      ByteString val) throws DirectoryException
1093  {
1094    boolean found = false;
1095    for (Attribute attr : attrList)
1096    {
1097      for (ByteString av : attr)
1098      {
1099        if (pwPolicyState.passwordIsPreEncoded(av))
1100        {
1101          if (passwordMatches(val, av))
1102          {
1103            builder.add(av);
1104            found = true;
1105          }
1106        }
1107        else if (av.equals(val))
1108        {
1109          builder.add(val);
1110          found = true;
1111        }
1112      }
1113    }
1114    return found;
1115  }
1116
1117  private boolean passwordMatches(ByteString val, ByteString av) throws DirectoryException
1118  {
1119    if (pwPolicyState.getAuthenticationPolicy().isAuthPasswordSyntax())
1120    {
1121      String[] components = AuthPasswordSyntax.decodeAuthPassword(av.toString());
1122      PasswordStorageScheme<?> scheme = DirectoryServer.getAuthPasswordStorageScheme(components[0]);
1123      return scheme != null && scheme.authPasswordMatches(val, components[1], components[2]);
1124    } else {
1125      String[] components = UserPasswordSyntax.decodeUserPassword(av.toString());
1126      PasswordStorageScheme<?> scheme = DirectoryServer.getPasswordStorageScheme(toLowerCase(components[0]));
1127      return scheme != null && scheme.passwordMatches(val, ByteString.valueOfUtf8(components[1]));
1128    }
1129  }
1130
1131  /**
1132   * Process an add modification and updates the entry appropriately.
1133   *
1134   * @param attr
1135   *          The attribute being added.
1136   * @throws DirectoryException
1137   *           If a problem occurs that should cause the modify operation to fail.
1138   */
1139  private void processAddModification(Attribute attr) throws DirectoryException
1140  {
1141    // Make sure that one or more values have been provided for the attribute.
1142    AttributeDescription attrDesc = attr.getAttributeDescription();
1143    if (attr.isEmpty())
1144    {
1145      throw newDirectoryException(currentEntry, ResultCode.PROTOCOL_ERROR,
1146          ERR_MODIFY_ADD_NO_VALUES.get(entryDN, attrDesc));
1147    }
1148
1149    if (mustCheckSchema())
1150    {
1151      // make sure that all the new values are valid according to the associated syntax.
1152      checkSchema(attr, ERR_MODIFY_ADD_INVALID_SYNTAX, ERR_MODIFY_ADD_INVALID_SYNTAX_NO_VALUE);
1153    }
1154
1155    // If the attribute to be added is the object class attribute
1156    // then make sure that all the object classes are known and not obsoleted.
1157    if (attrDesc.getAttributeType().isObjectClass())
1158    {
1159      validateObjectClasses(attr);
1160    }
1161
1162    // Add the provided attribute or merge an existing attribute with
1163    // the values of the new attribute. If there are any duplicates, then fail.
1164    List<ByteString> duplicateValues = new LinkedList<>();
1165    modifiedEntry.addAttribute(attr, duplicateValues);
1166    if (!duplicateValues.isEmpty() && !permissiveModify)
1167    {
1168      String duplicateValuesStr = Utils.joinAsString(", ", duplicateValues);
1169
1170      throw newDirectoryException(currentEntry,
1171          ResultCode.ATTRIBUTE_OR_VALUE_EXISTS,
1172          ERR_MODIFY_ADD_DUPLICATE_VALUE.get(entryDN, attrDesc, duplicateValuesStr));
1173    }
1174  }
1175
1176  private boolean mustCheckSchema()
1177  {
1178    return !isSynchronizationOperation() && DirectoryServer.checkSchema();
1179  }
1180
1181  /**
1182   * Verifies that all the new values are valid according to the associated syntax.
1183   *
1184   * @throws DirectoryException
1185   *           If any of the new values violate the server schema configuration and server is
1186   *           configured to reject violations.
1187   */
1188  private void checkSchema(Attribute attr,
1189      Arg4<Object, Object, Object, Object> invalidSyntaxErrorMsg,
1190      Arg3<Object, Object, Object> invalidSyntaxNoValueErrorMsg) throws DirectoryException
1191  {
1192    AcceptRejectWarn syntaxPolicy = DirectoryServer.getSyntaxEnforcementPolicy();
1193    AttributeDescription attrDesc = attr.getAttributeDescription();
1194    Syntax syntax = attrDesc.getAttributeType().getSyntax();
1195
1196    LocalizableMessageBuilder invalidReason = new LocalizableMessageBuilder();
1197    for (ByteString v : attr)
1198    {
1199      if (!syntax.valueIsAcceptable(v, invalidReason))
1200      {
1201        LocalizableMessage msg = isHumanReadable(syntax)
1202            ? invalidSyntaxErrorMsg.get(entryDN, attrDesc, v, invalidReason)
1203            : invalidSyntaxNoValueErrorMsg.get(entryDN, attrDesc, invalidReason);
1204
1205        switch (syntaxPolicy)
1206        {
1207        case REJECT:
1208          throw newDirectoryException(currentEntry, ResultCode.INVALID_ATTRIBUTE_SYNTAX, msg);
1209
1210        case WARN:
1211          // FIXME remove next line of code. According to Matt, since this is
1212          // just a warning, the code should not set the resultCode
1213          setResultCode(ResultCode.INVALID_ATTRIBUTE_SYNTAX);
1214          logger.error(msg);
1215          invalidReason = new LocalizableMessageBuilder();
1216          break;
1217        }
1218      }
1219    }
1220  }
1221
1222  private boolean isHumanReadable(Syntax syntax)
1223  {
1224    return syntax.isHumanReadable() && !syntax.isBEREncodingRequired();
1225  }
1226
1227  /**
1228   * Ensures that the provided object class attribute contains known
1229   * non-obsolete object classes.
1230   *
1231   * @param attr
1232   *          The object class attribute to validate.
1233   * @throws DirectoryException
1234   *           If the attribute contained unknown or obsolete object
1235   *           classes.
1236   */
1237  private void validateObjectClasses(Attribute attr) throws DirectoryException
1238  {
1239    final AttributeType attrType = attr.getAttributeDescription().getAttributeType();
1240    Reject.ifFalse(attrType.isObjectClass());
1241
1242    for (ByteString v : attr)
1243    {
1244      String name = v.toString();
1245      ObjectClass oc = DirectoryServer.getSchema().getObjectClass(name);
1246      if (oc.isPlaceHolder())
1247      {
1248        throw newDirectoryException(currentEntry,
1249            ResultCode.OBJECTCLASS_VIOLATION,
1250            ERR_ENTRY_ADD_UNKNOWN_OC.get(name, entryDN));
1251      }
1252      else if (oc.isObsolete())
1253      {
1254        throw newDirectoryException(currentEntry,
1255            ResultCode.CONSTRAINT_VIOLATION,
1256            ERR_ENTRY_ADD_OBSOLETE_OC.get(name, entryDN));
1257      }
1258    }
1259  }
1260
1261
1262
1263  /**
1264   * Process a delete modification and updates the entry appropriately.
1265   *
1266   * @param attr
1267   *          The attribute being deleted.
1268   * @throws DirectoryException
1269   *           If a problem occurs that should cause the modify operation to fail.
1270   */
1271  private void processDeleteModification(Attribute attr) throws DirectoryException
1272  {
1273    // Remove the specified attribute values or the entire attribute from the value.
1274    // If there are any specified values that were not present, then fail.
1275    // If the RDN attribute value would be removed, then fail.
1276    List<ByteString> missingValues = new LinkedList<>();
1277    boolean attrExists = modifiedEntry.removeAttribute(attr, missingValues);
1278
1279    AttributeDescription attrDesc = attr.getAttributeDescription();
1280    if (attrExists)
1281    {
1282      if (missingValues.isEmpty())
1283      {
1284        AttributeType t = attrDesc.getAttributeType();
1285
1286        RDN rdn = modifiedEntry.getName().rdn();
1287        if (rdn != null
1288            && rdn.hasAttributeType(t)
1289            && !modifiedEntry.hasValue(attrDesc, rdn.getAttributeValue(t)))
1290        {
1291          throw newDirectoryException(currentEntry,
1292              ResultCode.NOT_ALLOWED_ON_RDN,
1293              ERR_MODIFY_DELETE_RDN_ATTR.get(entryDN, attrDesc));
1294        }
1295      }
1296      else if (!permissiveModify)
1297      {
1298        String missingValuesStr = Utils.joinAsString(", ", missingValues);
1299
1300        throw newDirectoryException(currentEntry, ResultCode.NO_SUCH_ATTRIBUTE,
1301            ERR_MODIFY_DELETE_MISSING_VALUES.get(entryDN, attrDesc, missingValuesStr));
1302      }
1303    }
1304    else if (!permissiveModify)
1305    {
1306      throw newDirectoryException(currentEntry, ResultCode.NO_SUCH_ATTRIBUTE,
1307          ERR_MODIFY_DELETE_NO_SUCH_ATTR.get(entryDN, attrDesc));
1308    }
1309  }
1310
1311
1312
1313  /**
1314   * Process a replace modification and updates the entry appropriately.
1315   *
1316   * @param attr
1317   *          The attribute being replaced.
1318   * @throws DirectoryException
1319   *           If a problem occurs that should cause the modify operation to fail.
1320   */
1321  private void processReplaceModification(Attribute attr) throws DirectoryException
1322  {
1323    if (mustCheckSchema())
1324    {
1325      // make sure that all the new values are valid according to the associated syntax.
1326      checkSchema(attr, ERR_MODIFY_REPLACE_INVALID_SYNTAX, ERR_MODIFY_REPLACE_INVALID_SYNTAX_NO_VALUE);
1327    }
1328
1329    // If the attribute to be replaced is the object class attribute
1330    // then make sure that all the object classes are known and not obsoleted.
1331    AttributeDescription attrDesc = attr.getAttributeDescription();
1332    AttributeType t = attrDesc.getAttributeType();
1333    if (t.isObjectClass())
1334    {
1335      validateObjectClasses(attr);
1336    }
1337
1338    // Replace the provided attribute.
1339    modifiedEntry.replaceAttribute(attr);
1340
1341    // Make sure that the RDN attribute value(s) has not been removed.
1342    RDN rdn = modifiedEntry.getName().rdn();
1343    if (rdn != null
1344        && rdn.hasAttributeType(t)
1345        && !modifiedEntry.hasValue(attrDesc, rdn.getAttributeValue(t)))
1346    {
1347      throw newDirectoryException(modifiedEntry, ResultCode.NOT_ALLOWED_ON_RDN,
1348          ERR_MODIFY_DELETE_RDN_ATTR.get(entryDN, attrDesc));
1349    }
1350  }
1351
1352  /**
1353   * Process an increment modification and updates the entry appropriately.
1354   *
1355   * @param attr
1356   *          The attribute being incremented.
1357   * @throws DirectoryException
1358   *           If a problem occurs that should cause the modify operation to fail.
1359   */
1360  private void processIncrementModification(Attribute attr) throws DirectoryException
1361  {
1362    // The specified attribute type must not be an RDN attribute.
1363    AttributeDescription attrDesc = attr.getAttributeDescription();
1364    AttributeType t = attrDesc.getAttributeType();
1365    RDN rdn = modifiedEntry.getName().rdn();
1366    if (rdn != null && rdn.hasAttributeType(t))
1367    {
1368      throw newDirectoryException(modifiedEntry, ResultCode.NOT_ALLOWED_ON_RDN,
1369          ERR_MODIFY_INCREMENT_RDN.get(entryDN, attrDesc));
1370    }
1371
1372    // The provided attribute must have a single value, and it must be an integer
1373    if (attr.isEmpty())
1374    {
1375      throw newDirectoryException(modifiedEntry, ResultCode.PROTOCOL_ERROR,
1376          ERR_MODIFY_INCREMENT_REQUIRES_VALUE.get(entryDN, attrDesc));
1377    }
1378    else if (attr.size() > 1)
1379    {
1380      throw newDirectoryException(modifiedEntry, ResultCode.PROTOCOL_ERROR,
1381          ERR_MODIFY_INCREMENT_REQUIRES_SINGLE_VALUE.get(entryDN, attrDesc));
1382    }
1383
1384    BigInteger incrementValue = parseLdapInteger(attr.iterator().next(), attrDesc, entryDN);
1385
1386    // Get the attribute that is to be incremented.
1387    Attribute modifiedAttr = modifiedEntry.getExactAttribute(attrDesc);
1388    if (modifiedAttr == null)
1389    {
1390      throw newDirectoryException(modifiedEntry,
1391          ResultCode.CONSTRAINT_VIOLATION,
1392          ERR_MODIFY_INCREMENT_REQUIRES_EXISTING_VALUE.get(entryDN, attrDesc));
1393    }
1394
1395    // Increment each attribute value by the specified amount.
1396    AttributeDescription modifiedAttrDesc = modifiedAttr.getAttributeDescription();
1397    AttributeBuilder builder = new AttributeBuilder(modifiedAttrDesc);
1398    for (ByteString existingValue : modifiedAttr)
1399    {
1400      BigInteger currentValue = parseLdapInteger(existingValue, modifiedAttrDesc, entryDN);
1401      builder.add(currentValue.add(incrementValue).toString());
1402    }
1403
1404    // Replace the existing attribute with the incremented version.
1405    modifiedEntry.replaceAttribute(builder.toAttribute());
1406  }
1407
1408  private BigInteger parseLdapInteger(ByteString v, AttributeDescription attrDesc, DN entryDN)
1409    throws DirectoryException
1410  {
1411    try
1412    {
1413      return new BigInteger(v.toString());
1414    }
1415    catch (Exception e)
1416    {
1417      logger.traceException(e);
1418
1419      throw new DirectoryException(
1420              ResultCode.INVALID_ATTRIBUTE_SYNTAX,
1421              ERR_MODIFY_INCREMENT_REQUIRES_INTEGER_VALUE.get(entryDN, attrDesc, entryDN),
1422              e);
1423    }
1424  }
1425
1426  /**
1427   * Performs additional preliminary processing that is required for a password change.
1428   *
1429   * @throws DirectoryException
1430   *           If a problem occurs that should cause the modify operation to fail.
1431   */
1432  private void performAdditionalPasswordChangedProcessing() throws DirectoryException
1433  {
1434    if (!passwordChanged)
1435    {
1436      // Nothing to do.
1437      return;
1438    }
1439
1440    // If it was a self change, then see if the current password was provided
1441    // and handle accordingly.
1442    final PasswordPolicy authPolicy = pwPolicyState.getAuthenticationPolicy();
1443    if (selfChange
1444        && authPolicy.isPasswordChangeRequiresCurrentPassword()
1445        && !currentPasswordProvided)
1446    {
1447      pwpErrorType = PasswordPolicyErrorType.MUST_SUPPLY_OLD_PASSWORD;
1448      throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
1449          ERR_MODIFY_PW_CHANGE_REQUIRES_CURRENT_PW.get());
1450    }
1451
1452
1453    // If this change would result in multiple password values, then see if that's OK.
1454    if (numPasswords > 1 && !authPolicy.isAllowMultiplePasswordValues())
1455    {
1456      pwpErrorType = PasswordPolicyErrorType.PASSWORD_MOD_NOT_ALLOWED;
1457      throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION,
1458          ERR_MODIFY_MULTIPLE_PASSWORDS_NOT_ALLOWED.get());
1459    }
1460
1461
1462    // If any of the password values should be validated, then do so now.
1463    if (newPasswords != null
1464        && (selfChange || !authPolicy.isSkipValidationForAdministrators()))
1465    {
1466      HashSet<ByteString> clearPasswords = new HashSet<>(pwPolicyState.getClearPasswords());
1467      if (currentPasswords != null)
1468      {
1469        clearPasswords.addAll(currentPasswords);
1470      }
1471
1472      for (ByteString v : newPasswords)
1473      {
1474        LocalizableMessageBuilder invalidReason = new LocalizableMessageBuilder();
1475        if (! pwPolicyState.passwordIsAcceptable(this, modifiedEntry,
1476                                 v, clearPasswords, invalidReason))
1477        {
1478          pwpErrorType = PasswordPolicyErrorType.INSUFFICIENT_PASSWORD_QUALITY;
1479          throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION,
1480              ERR_MODIFY_PW_VALIDATION_FAILED.get(invalidReason));
1481        }
1482      }
1483    }
1484
1485    // If we should check the password history, then do so now.
1486    if (newPasswords != null && pwPolicyState.maintainHistory())
1487    {
1488      for (ByteString v : newPasswords)
1489      {
1490        if (pwPolicyState.isPasswordInHistory(v)
1491            && (selfChange || !authPolicy.isSkipValidationForAdministrators()))
1492        {
1493          pwpErrorType = PasswordPolicyErrorType.PASSWORD_IN_HISTORY;
1494          throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION,
1495              ERR_MODIFY_PW_IN_HISTORY.get());
1496        }
1497      }
1498
1499      pwPolicyState.updatePasswordHistory();
1500    }
1501
1502
1503    wasLocked = pwPolicyState.isLocked();
1504
1505    // Update the password policy state attributes in the user's entry.  If the
1506    // modification fails, then these changes won't be applied.
1507    pwPolicyState.setPasswordChangedTime();
1508    pwPolicyState.clearFailureLockout();
1509    pwPolicyState.clearGraceLoginTimes();
1510    pwPolicyState.clearWarnedTime();
1511
1512    if (authPolicy.isForceChangeOnAdd() || authPolicy.isForceChangeOnReset())
1513    {
1514      if (selfChange)
1515      {
1516        pwPolicyState.setMustChangePassword(false);
1517      }
1518      else
1519      {
1520        if (pwpErrorType == null && authPolicy.isForceChangeOnReset())
1521        {
1522          pwpErrorType = PasswordPolicyErrorType.CHANGE_AFTER_RESET;
1523        }
1524
1525        pwPolicyState.setMustChangePassword(authPolicy.isForceChangeOnReset());
1526      }
1527    }
1528
1529    if (authPolicy.getRequireChangeByTime() > 0)
1530    {
1531      pwPolicyState.setRequiredChangeTime();
1532    }
1533
1534    modifications.addAll(pwPolicyState.getModifications());
1535    modifiedEntry.applyModifications(pwPolicyState.getModifications());
1536  }
1537
1538  /** Generate any password policy account status notifications as a result of modify processing. */
1539  private void generatePwpAccountStatusNotifications()
1540  {
1541    if (passwordChanged)
1542    {
1543      if (selfChange)
1544      {
1545        AuthenticationInfo authInfo = clientConnection.getAuthenticationInfo();
1546        if (authInfo.getAuthenticationDN().equals(modifiedEntry.getName()))
1547        {
1548          clientConnection.setMustChangePassword(false);
1549        }
1550
1551        generateAccountStatusNotificationForPwds(PASSWORD_CHANGED, INFO_MODIFY_PASSWORD_CHANGED.get());
1552      }
1553      else
1554      {
1555        generateAccountStatusNotificationForPwds(PASSWORD_RESET, INFO_MODIFY_PASSWORD_RESET.get());
1556      }
1557    }
1558
1559    if (enabledStateChanged)
1560    {
1561      if (isEnabled)
1562      {
1563        generateAccountStatusNotificationNoPwds(ACCOUNT_ENABLED, INFO_MODIFY_ACCOUNT_ENABLED.get());
1564      }
1565      else
1566      {
1567        generateAccountStatusNotificationNoPwds(ACCOUNT_DISABLED, INFO_MODIFY_ACCOUNT_DISABLED.get());
1568      }
1569    }
1570
1571    if (wasLocked)
1572    {
1573      generateAccountStatusNotificationNoPwds(ACCOUNT_UNLOCKED, INFO_MODIFY_ACCOUNT_UNLOCKED.get());
1574    }
1575  }
1576
1577  private void generateAccountStatusNotificationNoPwds(
1578      AccountStatusNotificationType notificationType, LocalizableMessage message)
1579  {
1580    pwPolicyState.generateAccountStatusNotification(notificationType, modifiedEntry, message,
1581        AccountStatusNotification.createProperties(pwPolicyState, false, -1, null, null));
1582  }
1583
1584  private void generateAccountStatusNotificationForPwds(
1585      AccountStatusNotificationType notificationType, LocalizableMessage message)
1586  {
1587    pwPolicyState.generateAccountStatusNotification(notificationType, modifiedEntry, message,
1588        AccountStatusNotification.createProperties(pwPolicyState, false, -1, currentPasswords, newPasswords));
1589  }
1590
1591  /**
1592   * Handle conflict resolution.
1593   *
1594   * @return {@code true} if processing should continue for the operation, or {@code false} if not.
1595   */
1596  private boolean handleConflictResolution() {
1597      for (SynchronizationProvider<?> provider : getSynchronizationProviders()) {
1598          try {
1599              SynchronizationProviderResult result =
1600                  provider.handleConflictResolution(this);
1601              if (! result.continueProcessing()) {
1602                  setResultCodeAndMessageNoInfoDisclosure(modifiedEntry,
1603                      result.getResultCode(), result.getErrorMessage());
1604                  setMatchedDN(result.getMatchedDN());
1605                  setReferralURLs(result.getReferralURLs());
1606                  return false;
1607              }
1608          } catch (DirectoryException de) {
1609              logger.traceException(de);
1610              logger.error(ERR_MODIFY_SYNCH_CONFLICT_RESOLUTION_FAILED,
1611                  getConnectionID(), getOperationID(), getExceptionMessage(de));
1612              setResponseData(de);
1613              return false;
1614          }
1615      }
1616      return true;
1617  }
1618
1619  /**
1620   * Process pre operation.
1621   * @return  {@code true} if processing should continue for the operation, or
1622   *          {@code false} if not.
1623   */
1624  private boolean processPreOperation() {
1625      for (SynchronizationProvider<?> provider : getSynchronizationProviders()) {
1626          try {
1627              if (!processOperationResult(this, provider.doPreOperation(this))) {
1628                  return false;
1629              }
1630          } catch (DirectoryException de) {
1631              logger.traceException(de);
1632              logger.error(ERR_MODIFY_SYNCH_PREOP_FAILED, getConnectionID(),
1633                      getOperationID(), getExceptionMessage(de));
1634              setResponseData(de);
1635              return false;
1636          }
1637      }
1638      return true;
1639  }
1640
1641  /** Invoke post operation synchronization providers. */
1642  private void processSynchPostOperationPlugins() {
1643      for (SynchronizationProvider<?> provider : getSynchronizationProviders()) {
1644          try {
1645              provider.doPostOperation(this);
1646          } catch (DirectoryException de) {
1647              logger.traceException(de);
1648              logger.error(ERR_MODIFY_SYNCH_POSTOP_FAILED, getConnectionID(),
1649                      getOperationID(), getExceptionMessage(de));
1650              setResponseData(de);
1651              return;
1652          }
1653      }
1654  }
1655}