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