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-2010 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.List;
031import java.util.Map;
032import java.util.concurrent.atomic.AtomicBoolean;
033
034import org.forgerock.i18n.LocalizableMessage;
035import org.forgerock.i18n.LocalizableMessageBuilder;
036import org.forgerock.i18n.slf4j.LocalizedLogger;
037import org.forgerock.opendj.ldap.ByteString;
038import org.forgerock.opendj.ldap.ResultCode;
039import org.forgerock.opendj.ldap.schema.Syntax;
040import org.opends.server.api.AccessControlHandler;
041import org.opends.server.api.AuthenticationPolicy;
042import org.opends.server.api.Backend;
043import org.opends.server.api.ClientConnection;
044import org.opends.server.api.PasswordStorageScheme;
045import org.opends.server.api.PasswordValidator;
046import org.opends.server.api.SynchronizationProvider;
047import org.opends.server.controls.LDAPAssertionRequestControl;
048import org.opends.server.controls.LDAPPostReadRequestControl;
049import org.opends.server.controls.PasswordPolicyErrorType;
050import org.opends.server.controls.PasswordPolicyResponseControl;
051import org.opends.server.core.AccessControlConfigManager;
052import org.opends.server.core.AddOperation;
053import org.opends.server.core.AddOperationWrapper;
054import org.opends.server.core.DirectoryServer;
055import org.opends.server.core.PasswordPolicy;
056import org.opends.server.core.PersistentSearch;
057import org.opends.server.schema.AuthPasswordSyntax;
058import org.opends.server.schema.UserPasswordSyntax;
059import org.opends.server.types.Attribute;
060import org.opends.server.types.AttributeBuilder;
061import org.opends.server.types.AttributeType;
062import org.opends.server.types.Attributes;
063import org.opends.server.types.CanceledOperationException;
064import org.opends.server.types.Control;
065import org.opends.server.types.DN;
066import org.opends.server.types.DirectoryException;
067import org.opends.server.types.Entry;
068import org.opends.server.types.LockManager.DNLock;
069import org.opends.server.types.ObjectClass;
070import org.opends.server.types.Privilege;
071import org.opends.server.types.RDN;
072import org.opends.server.types.SearchFilter;
073import org.opends.server.types.operation.PostOperationAddOperation;
074import org.opends.server.types.operation.PostResponseAddOperation;
075import org.opends.server.types.operation.PostSynchronizationAddOperation;
076import org.opends.server.types.operation.PreOperationAddOperation;
077import org.opends.server.util.TimeThread;
078
079import static org.opends.messages.CoreMessages.*;
080import static org.opends.server.config.ConfigConstants.*;
081import static org.opends.server.types.AbstractOperation.*;
082import static org.opends.server.core.DirectoryServer.*;
083import static org.opends.server.util.CollectionUtils.*;
084import static org.opends.server.util.ServerConstants.*;
085import static org.opends.server.util.StaticUtils.*;
086
087/**
088 * This class defines an operation used to add an entry in a local backend
089 * of the Directory Server.
090 */
091public class LocalBackendAddOperation
092       extends AddOperationWrapper
093       implements PreOperationAddOperation, PostOperationAddOperation,
094                  PostResponseAddOperation, PostSynchronizationAddOperation
095{
096  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
097
098  /** The backend in which the entry is to be added. */
099  private Backend<?> backend;
100
101  /** Indicates whether the request includes the LDAP no-op control. */
102  private boolean noOp;
103
104  /** The DN of the entry to be added. */
105  private DN entryDN;
106
107  /** The entry being added to the server. */
108  private Entry entry;
109
110  /** The post-read request control included in the request, if applicable. */
111  private LDAPPostReadRequestControl postReadRequest;
112
113  /** The set of object classes for the entry to add. */
114  private Map<ObjectClass, String> objectClasses;
115
116  /** The set of operational attributes for the entry to add. */
117  private Map<AttributeType, List<Attribute>> operationalAttributes;
118
119  /** The set of user attributes for the entry to add. */
120  private Map<AttributeType, List<Attribute>> userAttributes;
121
122  /**
123   * Creates a new operation that may be used to add a new entry in a
124   * local backend of the Directory Server.
125   *
126   * @param add The operation to enhance.
127   */
128  public LocalBackendAddOperation(AddOperation add)
129  {
130    super(add);
131
132    LocalBackendWorkflowElement.attachLocalOperation (add, this);
133  }
134
135
136
137  /**
138   * Retrieves the entry to be added to the server.  Note that this will not be
139   * available to pre-parse plugins or during the conflict resolution portion of
140   * the synchronization processing.
141   *
142   * @return  The entry to be added to the server, or <CODE>null</CODE> if it is
143   *          not yet available.
144   */
145  @Override
146  public final Entry getEntryToAdd()
147  {
148    return entry;
149  }
150
151
152
153  /**
154   * Process this add operation against a local backend.
155   *
156   * @param wfe
157   *          The local backend work-flow element.
158   * @throws CanceledOperationException
159   *           if this operation should be cancelled
160   */
161  public void processLocalAdd(final LocalBackendWorkflowElement wfe)
162      throws CanceledOperationException
163  {
164    this.backend = wfe.getBackend();
165    ClientConnection clientConnection = getClientConnection();
166
167    // Check for a request to cancel this operation.
168    checkIfCanceled(false);
169
170    try
171    {
172      AtomicBoolean executePostOpPlugins = new AtomicBoolean(false);
173      processAdd(clientConnection, executePostOpPlugins);
174
175      // Invoke the post-operation or post-synchronization add plugins.
176      if (isSynchronizationOperation())
177      {
178        if (getResultCode() == ResultCode.SUCCESS)
179        {
180          getPluginConfigManager().invokePostSynchronizationAddPlugins(this);
181        }
182      }
183      else if (executePostOpPlugins.get())
184      {
185        // FIXME -- Should this also be done while holding the locks?
186        if (!processOperationResult(this, getPluginConfigManager().invokePostOperationAddPlugins(this)))
187        {
188          return;
189        }
190      }
191    }
192    finally
193    {
194      LocalBackendWorkflowElement.filterNonDisclosableMatchedDN(this);
195    }
196
197    // Register a post-response call-back which will notify persistent
198    // searches and change listeners.
199    if (getResultCode() == ResultCode.SUCCESS)
200    {
201      registerPostResponseCallback(new Runnable()
202      {
203        @Override
204        public void run()
205        {
206          for (PersistentSearch psearch : backend.getPersistentSearches())
207          {
208            psearch.processAdd(entry);
209          }
210        }
211      });
212    }
213  }
214
215  private void processAdd(ClientConnection clientConnection,
216      AtomicBoolean executePostOpPlugins) throws CanceledOperationException
217  {
218    // Process the entry DN and set of attributes to convert them from their
219    // raw forms as provided by the client to the forms required for the rest
220    // of the add processing.
221    entryDN = getEntryDN();
222    if (entryDN == null)
223    {
224      return;
225    }
226
227    // Check for a request to cancel this operation.
228    checkIfCanceled(false);
229
230    // Grab a write lock on the target entry. We'll need to do this
231    // eventually anyway, and we want to make sure that the two locks are
232    // always released when exiting this method, no matter what. Since
233    // the entry shouldn't exist yet, locking earlier than necessary
234    // shouldn't cause a problem.
235    final DNLock entryLock = DirectoryServer.getLockManager().tryWriteLockEntry(entryDN);
236    try
237    {
238      if (entryLock == null)
239      {
240        setResultCode(ResultCode.BUSY);
241        appendErrorMessage(ERR_ADD_CANNOT_LOCK_ENTRY.get(entryDN));
242        return;
243      }
244
245      DN parentDN = entryDN.getParentDNInSuffix();
246      if (parentDN == null && !DirectoryServer.isNamingContext(entryDN))
247      {
248        if (entryDN.isRootDN())
249        {
250          // This is not fine.  The root DSE cannot be added.
251          throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_ADD_CANNOT_ADD_ROOT_DSE.get());
252        }
253        else
254        {
255          // The entry doesn't have a parent but isn't a suffix.  This is not
256          // allowed.
257          throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, ERR_ADD_ENTRY_NOT_SUFFIX.get(entryDN));
258        }
259      }
260
261      // Check for a request to cancel this operation.
262      checkIfCanceled(false);
263
264
265      // Invoke any conflict resolution processing that might be needed by the
266      // synchronization provider.
267      for (SynchronizationProvider<?> provider : getSynchronizationProviders())
268      {
269        try
270        {
271          if (!processOperationResult(this, provider.handleConflictResolution(this)))
272          {
273            return;
274          }
275        }
276        catch (DirectoryException de)
277        {
278          logger.error(ERR_ADD_SYNCH_CONFLICT_RESOLUTION_FAILED,
279              getConnectionID(), getOperationID(), getExceptionMessage(de));
280          throw de;
281        }
282      }
283
284      objectClasses = getObjectClasses();
285      userAttributes = getUserAttributes();
286      operationalAttributes = getOperationalAttributes();
287
288      if (objectClasses == null
289          || userAttributes == null
290          || operationalAttributes == null)
291      {
292        return;
293      }
294
295      // If the attribute type is marked "NO-USER-MODIFICATION" then fail
296      // unless this is an internal operation or is related to
297      // synchronization in some way.
298      // This must be done before running the password policy code
299      // and any other code that may add attributes marked as
300      // "NO-USER-MODIFICATION"
301      //
302      // Note that doing this checks at this time
303      // of the processing does not make it possible for pre-parse plugins
304      // to add NO-USER-MODIFICATION attributes to the entry.
305      if (checkHasReadOnlyAttributes(userAttributes)
306          || checkHasReadOnlyAttributes(operationalAttributes))
307      {
308        return;
309      }
310
311
312      // Check to see if the entry already exists. We do this before
313      // checking whether the parent exists to ensure a referral entry
314      // above the parent results in a correct referral.
315      if (DirectoryServer.entryExists(entryDN))
316      {
317        setResultCodeAndMessageNoInfoDisclosure(entryDN,
318            ResultCode.ENTRY_ALREADY_EXISTS,
319            ERR_ADD_ENTRY_ALREADY_EXISTS.get(entryDN));
320        return;
321      }
322
323      // Get the parent entry, if it exists.
324      Entry parentEntry = null;
325      if (parentDN != null)
326      {
327        parentEntry = DirectoryServer.getEntry(parentDN);
328
329        if (parentEntry == null)
330        {
331          final DN matchedDN = findMatchedDN(parentDN);
332          setMatchedDN(matchedDN);
333
334          // The parent doesn't exist, so this add can't be successful.
335          if (matchedDN != null)
336          {
337            // check whether matchedDN allows to disclose info
338            setResultCodeAndMessageNoInfoDisclosure(matchedDN,
339                ResultCode.NO_SUCH_OBJECT, ERR_ADD_NO_PARENT.get(entryDN, parentDN));
340          }
341          else
342          {
343            // no matched DN either, so let's return normal error code
344            setResultCode(ResultCode.NO_SUCH_OBJECT);
345            appendErrorMessage(ERR_ADD_NO_PARENT.get(entryDN, parentDN));
346          }
347          return;
348        }
349      }
350
351      // Check to make sure that all of the RDN attributes are included as
352      // attribute values. If not, then either add them or report an error.
353      addRDNAttributesIfNecessary();
354
355      // Add any superior objectclass(s) missing in an entries
356      // objectclass map.
357      addSuperiorObjectClasses(objectClasses);
358
359      // Create an entry object to encapsulate the set of attributes and
360      // objectclasses.
361      entry = new Entry(entryDN, objectClasses, userAttributes,
362              operationalAttributes);
363
364      // Check to see if the entry includes a privilege specification. If so,
365      // then the requester must have the PRIVILEGE_CHANGE privilege.
366      AttributeType privType = DirectoryServer.getAttributeTypeOrDefault(OP_ATTR_PRIVILEGE_NAME);
367      if (entry.hasAttribute(privType)
368          && !clientConnection.hasPrivilege(Privilege.PRIVILEGE_CHANGE, this))
369      {
370        appendErrorMessage(ERR_ADD_CHANGE_PRIVILEGE_INSUFFICIENT_PRIVILEGES.get());
371        setResultCode(ResultCode.INSUFFICIENT_ACCESS_RIGHTS);
372        return;
373      }
374
375      // If it's not a synchronization operation, then check
376      // to see if the entry contains one or more passwords and if they
377      // are valid in accordance with the password policies associated with
378      // the user. Also perform any encoding that might be required by
379      // password storage schemes.
380      if (!isSynchronizationOperation())
381      {
382        handlePasswordPolicy();
383      }
384
385      // If the server is configured to check schema and the
386      // operation is not a synchronization operation,
387      // check to see if the entry is valid according to the server schema,
388      // and also whether its attributes are valid according to their syntax.
389      if (DirectoryServer.checkSchema() && !isSynchronizationOperation())
390      {
391        checkSchema(parentEntry);
392      }
393
394      // Get the backend in which the add is to be performed.
395      if (backend == null)
396      {
397        setResultCode(ResultCode.NO_SUCH_OBJECT);
398        appendErrorMessage(LocalizableMessage.raw("No backend for entry " + entryDN)); // TODO: i18n
399        return;
400      }
401
402      // Check to see if there are any controls in the request. If so, then
403      // see if there is any special processing required.
404      processControls(parentDN);
405
406      // Check to see if the client has permission to perform the add.
407
408      // FIXME: for now assume that this will check all permission
409      // pertinent to the operation. This includes proxy authorization
410      // and any other controls specified.
411
412      // FIXME: earlier checks to see if the entry already exists or
413      // if the parent entry does not exist may have already exposed
414      // sensitive information to the client.
415      try
416      {
417        if (!getAccessControlHandler().isAllowed(this))
418        {
419          setResultCodeAndMessageNoInfoDisclosure(entryDN,
420              ResultCode.INSUFFICIENT_ACCESS_RIGHTS,
421              ERR_ADD_AUTHZ_INSUFFICIENT_ACCESS_RIGHTS.get(entryDN));
422          return;
423        }
424      }
425      catch (DirectoryException e)
426      {
427        setResultCode(e.getResultCode());
428        appendErrorMessage(e.getMessageObject());
429        return;
430      }
431
432      // Check for a request to cancel this operation.
433      checkIfCanceled(false);
434
435      // If the operation is not a synchronization operation,
436      // Invoke the pre-operation add plugins.
437      if (!isSynchronizationOperation())
438      {
439        executePostOpPlugins.set(true);
440        if (!processOperationResult(this, getPluginConfigManager().invokePreOperationAddPlugins(this)))
441        {
442          return;
443        }
444      }
445
446      LocalBackendWorkflowElement.checkIfBackendIsWritable(backend, this,
447          entryDN, ERR_ADD_SERVER_READONLY, ERR_ADD_BACKEND_READONLY);
448
449      if (noOp)
450      {
451        appendErrorMessage(INFO_ADD_NOOP.get());
452        setResultCode(ResultCode.NO_OPERATION);
453      }
454      else
455      {
456        for (SynchronizationProvider<?> provider : getSynchronizationProviders())
457        {
458          try
459          {
460            if (!processOperationResult(this, provider.doPreOperation(this)))
461            {
462              return;
463            }
464          }
465          catch (DirectoryException de)
466          {
467            logger.error(ERR_ADD_SYNCH_PREOP_FAILED, getConnectionID(),
468                getOperationID(), getExceptionMessage(de));
469            throw de;
470          }
471        }
472
473        backend.addEntry(entry, this);
474      }
475
476      LocalBackendWorkflowElement.addPostReadResponse(this, postReadRequest,
477          entry);
478
479      if (!noOp)
480      {
481        setResultCode(ResultCode.SUCCESS);
482      }
483    }
484    catch (DirectoryException de)
485    {
486      logger.traceException(de);
487
488      setResponseData(de);
489    }
490    finally
491    {
492      if (entryLock != null)
493      {
494        entryLock.unlock();
495      }
496      processSynchPostOperationPlugins();
497    }
498  }
499
500
501
502  private void processSynchPostOperationPlugins()
503  {
504    for (SynchronizationProvider<?> provider : getSynchronizationProviders())
505    {
506      try
507      {
508        provider.doPostOperation(this);
509      }
510      catch (DirectoryException de)
511      {
512        logger.traceException(de);
513        logger.error(ERR_ADD_SYNCH_POSTOP_FAILED, getConnectionID(),
514            getOperationID(), getExceptionMessage(de));
515        setResponseData(de);
516        break;
517      }
518    }
519  }
520
521  private DN findMatchedDN(DN entryDN)
522  {
523    try
524    {
525      DN matchedDN = entryDN.getParentDNInSuffix();
526      while (matchedDN != null)
527      {
528        if (DirectoryServer.entryExists(matchedDN))
529        {
530          return matchedDN;
531        }
532
533        matchedDN = matchedDN.getParentDNInSuffix();
534      }
535    }
536    catch (Exception e)
537    {
538      logger.traceException(e);
539    }
540    return null;
541  }
542
543  private boolean checkHasReadOnlyAttributes(
544      Map<AttributeType, List<Attribute>> attributes) throws DirectoryException
545  {
546    for (AttributeType at : attributes.keySet())
547    {
548      if (at.isNoUserModification()
549          && !isInternalOperation()
550          && !isSynchronizationOperation())
551      {
552        setResultCodeAndMessageNoInfoDisclosure(entryDN,
553            ResultCode.CONSTRAINT_VIOLATION,
554            ERR_ADD_ATTR_IS_NO_USER_MOD.get(entryDN, at.getNameOrOID()));
555        return true;
556      }
557    }
558    return false;
559  }
560
561  private DirectoryException newDirectoryException(DN entryDN,
562      ResultCode resultCode, LocalizableMessage message) throws DirectoryException
563  {
564    return LocalBackendWorkflowElement.newDirectoryException(this, null,
565        entryDN, resultCode, message, ResultCode.INSUFFICIENT_ACCESS_RIGHTS,
566        ERR_ADD_AUTHZ_INSUFFICIENT_ACCESS_RIGHTS.get(entryDN));
567  }
568
569  private void setResultCodeAndMessageNoInfoDisclosure(DN entryDN,
570      ResultCode resultCode, LocalizableMessage message) throws DirectoryException
571  {
572    LocalBackendWorkflowElement.setResultCodeAndMessageNoInfoDisclosure(this,
573        null, entryDN, resultCode, message,
574        ResultCode.INSUFFICIENT_ACCESS_RIGHTS,
575        ERR_ADD_AUTHZ_INSUFFICIENT_ACCESS_RIGHTS.get(entryDN));
576  }
577
578
579
580  /**
581   * Adds any missing RDN attributes to the entry.
582   *
583   * @throws  DirectoryException  If the entry is missing one or more RDN
584   *                              attributes and the server is configured to
585   *                              reject such entries.
586   */
587  private void addRDNAttributesIfNecessary() throws DirectoryException
588  {
589    RDN rdn = entryDN.rdn();
590    int numAVAs = rdn.getNumValues();
591    for (int i=0; i < numAVAs; i++)
592    {
593      AttributeType  t = rdn.getAttributeType(i);
594      ByteString     v = rdn.getAttributeValue(i);
595      String         n = rdn.getAttributeName(i);
596      if (t.isOperational())
597      {
598        addRDNAttributesIfNecessary(operationalAttributes, t, v, n);
599      }
600      else
601      {
602        addRDNAttributesIfNecessary(userAttributes, t, v, n);
603      }
604    }
605  }
606
607
608
609  private void addRDNAttributesIfNecessary(
610      Map<AttributeType, List<Attribute>> attributes, AttributeType t,
611      ByteString v, String n) throws DirectoryException
612  {
613    final List<Attribute> attrList = attributes.get(t);
614    if (attrList == null)
615    {
616      if (!isSynchronizationOperation()
617          && !DirectoryServer.addMissingRDNAttributes())
618      {
619        throw newDirectoryException(entryDN, ResultCode.CONSTRAINT_VIOLATION,
620            ERR_ADD_MISSING_RDN_ATTRIBUTE.get(entryDN, n));
621      }
622      attributes.put(t, newArrayList(Attributes.create(t, n, v)));
623      return;
624    }
625
626    for (int j = 0; j < attrList.size(); j++) {
627      Attribute a = attrList.get(j);
628      if (a.hasOptions())
629      {
630        continue;
631      }
632
633      if (!a.contains(v))
634      {
635        AttributeBuilder builder = new AttributeBuilder(a);
636        builder.add(v);
637        attrList.set(j, builder.toAttribute());
638      }
639
640      return;
641    }
642
643    // not found
644    if (!isSynchronizationOperation() && !DirectoryServer.addMissingRDNAttributes())
645    {
646      throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION,
647          ERR_ADD_MISSING_RDN_ATTRIBUTE.get(entryDN, n));
648    }
649    attrList.add(Attributes.create(t, n, v));
650  }
651
652
653
654  /**
655   * Adds the provided objectClass to the entry, along with its superior classes
656   * if appropriate.
657   *
658   * @param  objectClass  The objectclass to add to the entry.
659   */
660  public final void addObjectClassChain(ObjectClass objectClass)
661  {
662    Map<ObjectClass, String> objectClasses = getObjectClasses();
663    if (objectClasses != null){
664      if (! objectClasses.containsKey(objectClass))
665      {
666        objectClasses.put(objectClass, objectClass.getNameOrOID());
667      }
668
669      for(ObjectClass superiorClass : objectClass.getSuperiorClasses())
670      {
671        if (!objectClasses.containsKey(superiorClass))
672        {
673          addObjectClassChain(superiorClass);
674        }
675      }
676    }
677  }
678
679
680
681  /**
682   * Performs all password policy processing necessary for the provided add
683   * operation.
684   *
685   * @throws  DirectoryException  If a problem occurs while performing password
686   *                              policy processing for the add operation.
687   */
688  public final void handlePasswordPolicy()
689         throws DirectoryException
690  {
691    // Construct any virtual/collective attributes which might
692    // contain a value for the OP_ATTR_PWPOLICY_POLICY_DN attribute.
693    Entry copy = entry.duplicate(true);
694    AuthenticationPolicy policy = AuthenticationPolicy.forUser(copy, false);
695    if (!policy.isPasswordPolicy())
696    {
697      // The entry doesn't have a locally managed password, so no action is
698      // required.
699      return;
700    }
701    PasswordPolicy passwordPolicy = (PasswordPolicy) policy;
702
703    // See if a password was specified.
704    AttributeType passwordAttribute = passwordPolicy.getPasswordAttribute();
705    List<Attribute> attrList = entry.getAttribute(passwordAttribute);
706    if (attrList == null || attrList.isEmpty())
707    {
708      // The entry doesn't have a password, so no action is required.
709      return;
710    }
711    else if (attrList.size() > 1)
712    {
713      // This must mean there are attribute options, which we won't allow for
714      // passwords.
715      LocalizableMessage message = ERR_PWPOLICY_ATTRIBUTE_OPTIONS_NOT_ALLOWED.get(
716          passwordAttribute.getNameOrOID());
717      throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
718    }
719
720    Attribute passwordAttr = attrList.get(0);
721    if (passwordAttr.hasOptions())
722    {
723      LocalizableMessage message = ERR_PWPOLICY_ATTRIBUTE_OPTIONS_NOT_ALLOWED.get(
724          passwordAttribute.getNameOrOID());
725      throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
726    }
727
728    if (passwordAttr.isEmpty())
729    {
730      // This will be treated the same as not having a password.
731      return;
732    }
733
734    if (!isInternalOperation()
735        && !passwordPolicy.isAllowMultiplePasswordValues()
736        && passwordAttr.size() > 1)
737    {
738      // FIXME -- What if they're pre-encoded and might all be the
739      // same?
740      addPWPolicyControl(PasswordPolicyErrorType.PASSWORD_MOD_NOT_ALLOWED);
741
742      LocalizableMessage message = ERR_PWPOLICY_MULTIPLE_PW_VALUES_NOT_ALLOWED
743          .get(passwordAttribute.getNameOrOID());
744      throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
745    }
746
747    List<PasswordStorageScheme<?>> defaultStorageSchemes =
748         passwordPolicy.getDefaultPasswordStorageSchemes();
749    AttributeBuilder builder = new AttributeBuilder(passwordAttr, true);
750    for (ByteString value : passwordAttr)
751    {
752      // See if the password is pre-encoded.
753      if (passwordPolicy.isAuthPasswordSyntax())
754      {
755        if (AuthPasswordSyntax.isEncoded(value))
756        {
757          if (isInternalOperation()
758              || passwordPolicy.isAllowPreEncodedPasswords())
759          {
760            builder.add(value);
761            continue;
762          }
763          else
764          {
765            addPWPolicyControl(PasswordPolicyErrorType.INSUFFICIENT_PASSWORD_QUALITY);
766
767            throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION,
768                ERR_PWPOLICY_PREENCODED_NOT_ALLOWED.get(passwordAttribute.getNameOrOID()));
769          }
770        }
771      }
772      else if (UserPasswordSyntax.isEncoded(value))
773      {
774        if (isInternalOperation()
775            || passwordPolicy.isAllowPreEncodedPasswords())
776        {
777          builder.add(value);
778          continue;
779        }
780        else
781        {
782          addPWPolicyControl(PasswordPolicyErrorType.INSUFFICIENT_PASSWORD_QUALITY);
783
784          throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION,
785              ERR_PWPOLICY_PREENCODED_NOT_ALLOWED.get(passwordAttribute.getNameOrOID()));
786        }
787      }
788
789
790      // See if the password passes validation.  We should only do this if
791      // validation should be performed for administrators.
792      if (! passwordPolicy.isSkipValidationForAdministrators())
793      {
794        // There are never any current passwords for an add operation.
795        HashSet<ByteString> currentPasswords = new HashSet<>(0);
796        LocalizableMessageBuilder invalidReason = new LocalizableMessageBuilder();
797        // Work on a copy of the entry without the password to avoid
798        // false positives from some validators.
799        copy.removeAttribute(passwordAttribute);
800        for (PasswordValidator<?> validator :
801          passwordPolicy.getPasswordValidators())
802        {
803          if (! validator.passwordIsAcceptable(value, currentPasswords, this,
804                                               copy, invalidReason))
805          {
806            addPWPolicyControl(
807                 PasswordPolicyErrorType.INSUFFICIENT_PASSWORD_QUALITY);
808
809            throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION,
810                ERR_PWPOLICY_VALIDATION_FAILED.get(passwordAttribute.getNameOrOID(), invalidReason));
811          }
812        }
813      }
814
815
816      // Encode the password.
817      if (passwordPolicy.isAuthPasswordSyntax())
818      {
819        for (PasswordStorageScheme<?> s : defaultStorageSchemes)
820        {
821          builder.add(s.encodeAuthPassword(value));
822        }
823      }
824      else
825      {
826        for (PasswordStorageScheme<?> s : defaultStorageSchemes)
827        {
828          builder.add(s.encodePasswordWithScheme(value));
829        }
830      }
831    }
832
833
834    // Put the new encoded values in the entry.
835    entry.replaceAttribute(builder.toAttribute());
836
837
838    // Set the password changed time attribute.
839    Attribute changedTime = Attributes.create(
840        OP_ATTR_PWPOLICY_CHANGED_TIME, TimeThread.getGeneralizedTime());
841    entry.putAttribute(changedTime.getAttributeType(), newArrayList(changedTime));
842
843
844    // If we should force change on add, then set the appropriate flag.
845    if (passwordPolicy.isForceChangeOnAdd())
846    {
847      addPWPolicyControl(PasswordPolicyErrorType.CHANGE_AFTER_RESET);
848
849      Attribute reset = Attributes.create(OP_ATTR_PWPOLICY_RESET_REQUIRED, "TRUE");
850      entry.putAttribute(reset.getAttributeType(), newArrayList(reset));
851    }
852  }
853
854
855
856  /**
857   * Adds a password policy response control if the corresponding request
858   * control was included.
859   *
860   * @param  errorType  The error type to use for the response control.
861   */
862  private void addPWPolicyControl(PasswordPolicyErrorType errorType)
863  {
864    for (Control c : getRequestControls())
865    {
866      if (OID_PASSWORD_POLICY_CONTROL.equals(c.getOID()))
867      {
868        addResponseControl(new PasswordPolicyResponseControl(null, 0, errorType));
869      }
870    }
871  }
872
873
874
875  /**
876   * Verifies that the entry to be added conforms to the server schema.
877   *
878   * @param  parentEntry  The parent of the entry to add.
879   *
880   * @throws  DirectoryException  If the entry violates the server schema
881   *                              configuration.
882   */
883  private void checkSchema(Entry parentEntry) throws DirectoryException
884  {
885    LocalizableMessageBuilder invalidReason = new LocalizableMessageBuilder();
886    if (! entry.conformsToSchema(parentEntry, true, true, true, invalidReason))
887    {
888      throw new DirectoryException(ResultCode.OBJECTCLASS_VIOLATION,
889                                   invalidReason.toMessage());
890    }
891
892    invalidReason = new LocalizableMessageBuilder();
893    checkAttributes(invalidReason, userAttributes);
894    checkAttributes(invalidReason, operationalAttributes);
895
896
897    // See if the entry contains any attributes or object classes marked
898    // OBSOLETE.  If so, then reject the entry.
899    for (AttributeType at : userAttributes.keySet())
900    {
901      if (at.isObsolete())
902      {
903        throw newDirectoryException(entryDN, ResultCode.CONSTRAINT_VIOLATION,
904            WARN_ADD_ATTR_IS_OBSOLETE.get(entryDN, at.getNameOrOID()));
905      }
906    }
907
908    for (AttributeType at : operationalAttributes.keySet())
909    {
910      if (at.isObsolete())
911      {
912        throw newDirectoryException(entryDN, ResultCode.CONSTRAINT_VIOLATION,
913            WARN_ADD_ATTR_IS_OBSOLETE.get(entryDN, at.getNameOrOID()));
914      }
915    }
916
917    for (ObjectClass oc : objectClasses.keySet())
918    {
919      if (oc.isObsolete())
920      {
921        throw newDirectoryException(entryDN, ResultCode.CONSTRAINT_VIOLATION,
922            WARN_ADD_OC_IS_OBSOLETE.get(entryDN, oc.getNameOrOID()));
923      }
924    }
925  }
926
927
928  private void checkAttributes(LocalizableMessageBuilder invalidReason,
929      Map<AttributeType, List<Attribute>> attributes) throws DirectoryException
930  {
931    for (List<Attribute> attrList : attributes.values())
932    {
933      for (Attribute a : attrList)
934      {
935        Syntax syntax = a.getAttributeType().getSyntax();
936        if (syntax != null)
937        {
938          for (ByteString v : a)
939          {
940            if (!syntax.valueIsAcceptable(v, invalidReason))
941            {
942              LocalizableMessage message;
943              if (!syntax.isHumanReadable() || syntax.isBEREncodingRequired())
944              {
945                // Value is not human-readable
946                message = WARN_ADD_OP_INVALID_SYNTAX_NO_VALUE.
947                    get(entryDN, a.getName(), invalidReason);
948              }
949              else
950              {
951                message = WARN_ADD_OP_INVALID_SYNTAX.
952                    get(entryDN, v, a.getName(), invalidReason);
953              }
954
955              switch (DirectoryServer.getSyntaxEnforcementPolicy())
956              {
957              case REJECT:
958                throw new DirectoryException(
959                    ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
960              case WARN:
961                logger.error(message);
962              }
963            }
964          }
965        }
966      }
967    }
968  }
969
970  /**
971   * Processes the set of controls contained in the add request.
972   *
973   * @param  parentDN  The DN of the parent of the entry to add.
974   *
975   * @throws  DirectoryException  If there is a problem with any of the
976   *                              request controls.
977   */
978  private void processControls(DN parentDN) throws DirectoryException
979  {
980    LocalBackendWorkflowElement.evaluateProxyAuthControls(this);
981    LocalBackendWorkflowElement.removeAllDisallowedControls(parentDN, this);
982
983    List<Control> requestControls = getRequestControls();
984    if (requestControls != null && !requestControls.isEmpty())
985    {
986      for (Control c : requestControls)
987      {
988        final String  oid = c.getOID();
989
990        if (OID_LDAP_ASSERTION.equals(oid))
991        {
992          // RFC 4528 mandates support for Add operation basically
993          // suggesting an assertion on self. As daft as it may be
994          // we gonna have to support this for RFC compliance.
995          LDAPAssertionRequestControl assertControl =
996            getRequestControl(LDAPAssertionRequestControl.DECODER);
997
998          SearchFilter filter;
999          try
1000          {
1001            filter = assertControl.getSearchFilter();
1002          }
1003          catch (DirectoryException de)
1004          {
1005            logger.traceException(de);
1006
1007            throw newDirectoryException(entryDN, de.getResultCode(),
1008                ERR_ADD_CANNOT_PROCESS_ASSERTION_FILTER.get(
1009                    entryDN, de.getMessageObject()));
1010          }
1011
1012          // Check if the current user has permission to make this determination.
1013          if (!getAccessControlHandler().isAllowed(this, entry, filter))
1014          {
1015            throw new DirectoryException(
1016                ResultCode.INSUFFICIENT_ACCESS_RIGHTS,
1017                ERR_CONTROL_INSUFFICIENT_ACCESS_RIGHTS.get(oid));
1018          }
1019
1020          try
1021          {
1022            if (!filter.matchesEntry(entry))
1023            {
1024              throw newDirectoryException(entryDN, ResultCode.ASSERTION_FAILED,
1025                  ERR_ADD_ASSERTION_FAILED.get(entryDN));
1026            }
1027          }
1028          catch (DirectoryException de)
1029          {
1030            if (de.getResultCode() == ResultCode.ASSERTION_FAILED)
1031            {
1032              throw de;
1033            }
1034
1035            logger.traceException(de);
1036
1037            throw newDirectoryException(entryDN, de.getResultCode(),
1038                ERR_ADD_CANNOT_PROCESS_ASSERTION_FILTER.get(
1039                    entryDN, de.getMessageObject()));
1040          }
1041        }
1042        else if (OID_LDAP_NOOP_OPENLDAP_ASSIGNED.equals(oid))
1043        {
1044          noOp = true;
1045        }
1046        else if (OID_LDAP_READENTRY_POSTREAD.equals(oid))
1047        {
1048          postReadRequest =
1049                getRequestControl(LDAPPostReadRequestControl.DECODER);
1050        }
1051        else if (LocalBackendWorkflowElement.isProxyAuthzControl(oid))
1052        {
1053          continue;
1054        }
1055        else if (OID_PASSWORD_POLICY_CONTROL.equals(oid))
1056        {
1057          // We don't need to do anything here because it's already handled
1058          // in LocalBackendAddOperation.handlePasswordPolicy().
1059        }
1060        // NYI -- Add support for additional controls.
1061        else if (c.isCritical()
1062            && (backend == null || !backend.supportsControl(oid)))
1063        {
1064          throw newDirectoryException(entryDN,
1065              ResultCode.UNAVAILABLE_CRITICAL_EXTENSION,
1066              ERR_ADD_UNSUPPORTED_CRITICAL_CONTROL.get(entryDN, oid));
1067        }
1068      }
1069    }
1070  }
1071
1072  private AccessControlHandler<?> getAccessControlHandler()
1073  {
1074    return AccessControlConfigManager.getInstance().getAccessControlHandler();
1075  }
1076}