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