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