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