/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2008-2010 Sun Microsystems, Inc.
 */
package org.opends.server.authorization.dseecompat;



import static org.opends.messages.AccessControlMessages.*;
import static org.opends.server.authorization.dseecompat.Aci.*;
import static org.opends.server.config.ConfigConstants.*;
import static org.opends.server.loggers.ErrorLogger.*;
import static org.opends.server.loggers.debug.DebugLogger.*;
import static org.opends.server.schema.SchemaConstants.*;
import static org.opends.server.util.ServerConstants.*;
import static org.opends.server.util.StaticUtils.*;

import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.concurrent.locks.Lock;

import org.opends.messages.Message;
import org.opends.server.admin.std.server.DseeCompatAccessControlHandlerCfg;
import org.opends.server.api.AccessControlHandler;
import org.opends.server.api.ClientConnection;
import org.opends.server.backends.jeb.EntryContainer;
import org.opends.server.config.ConfigException;
import org.opends.server.controls.GetEffectiveRightsRequestControl;
import org.opends.server.core.DirectoryServer;
import org.opends.server.core.ExtendedOperation;
import org.opends.server.core.SearchOperation;
import org.opends.server.loggers.debug.DebugTracer;
import org.opends.server.protocols.internal.InternalClientConnection;
import org.opends.server.protocols.internal.InternalSearchOperation;
import org.opends.server.protocols.ldap.LDAPControl;
import org.opends.server.types.Attribute;
import org.opends.server.types.AttributeBuilder;
import org.opends.server.types.AttributeType;
import org.opends.server.types.AttributeValue;
import org.opends.server.types.AttributeValues;
import org.opends.server.types.AuthenticationInfo;
import org.opends.server.types.Control;
import org.opends.server.types.DN;
import org.opends.server.types.DebugLogLevel;
import org.opends.server.types.DereferencePolicy;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.Entry;
import org.opends.server.types.InitializationException;
import org.opends.server.types.LockManager;
import org.opends.server.types.Modification;
import org.opends.server.types.ModificationType;
import org.opends.server.types.Operation;
import org.opends.server.types.Privilege;
import org.opends.server.types.RDN;
import org.opends.server.types.ResultCode;
import org.opends.server.types.SearchFilter;
import org.opends.server.types.SearchResultEntry;
import org.opends.server.types.SearchResultReference;
import org.opends.server.types.SearchScope;
import org.opends.server.workflowelement.localbackend.*;



/**
 * The AciHandler class performs the main processing for the dseecompat
 * package.
 */
public final class AciHandler extends
    AccessControlHandler<DseeCompatAccessControlHandlerCfg>
{
  /**
   * String used to save a resource entry containing all the attributes
   * in the SearchOperation attachment list. This is only used during
   * geteffectiverights read right processing when all of an entry'ss
   * attributes need to examined.
   */
  public static final String ALL_ATTRS_RESOURCE_ENTRY =
      "allAttrsResourceEntry";

  /**
   * String used to indicate that the evaluating ACI had a all
   * operational attributes targetattr match (targetattr="+").
   */
  public static final String ALL_OP_ATTRS_MATCHED = "allOpAttrsMatched";

  /**
   * String used to indicate that the evaluating ACI had a all user
   * attributes targetattr match (targetattr="*").
   */
  public static final String ALL_USER_ATTRS_MATCHED =
      "allUserAttrsMatched";

  /**
   * String used to save the original authorization entry in an
   * operation attachment if a proxied authorization control was seen.
   */
  public static final String ORIG_AUTH_ENTRY = "origAuthorizationEntry";

  /**
   * Attribute type corresponding to "aci" attribute.
   */
  static AttributeType aciType;

  /**
   * Attribute type corresponding to global "ds-cfg-global-aci"
   * attribute.
   */
  static AttributeType globalAciType;

  /**
   * Attribute type corresponding to "debugsearchindex" attribute.
   */
  private static AttributeType debugSearchIndex;

  /*
   * DN corresponding to "debugsearchindex" attribute type.
   */
  private static DN debugSearchIndexDN;

  /**
   * Attribute type corresponding to the "ref" attribute type. Used in
   * the search reference access check.
   */
  private static AttributeType refAttrType;

  /**
   * The tracer object for the debug logger.
   */
  private static final DebugTracer TRACER = getTracer();

  static
  {
    initStatics();
  }



  // We initialize these for each new AciHandler so that we can clear
  // out the stale references that can occur during an in-core restart.
  private static void initStatics()
  {
    if ((aciType = DirectoryServer.getAttributeType("aci")) == null)
    {
      aciType = DirectoryServer.getDefaultAttributeType("aci");
    }

    if ((globalAciType =
        DirectoryServer.getAttributeType(ATTR_AUTHZ_GLOBAL_ACI)) == null)
    {
      globalAciType =
          DirectoryServer
              .getDefaultAttributeType(ATTR_AUTHZ_GLOBAL_ACI);
    }

    if ((debugSearchIndex =
        DirectoryServer
            .getAttributeType(EntryContainer.ATTR_DEBUG_SEARCH_INDEX)) == null)
    {
      debugSearchIndex =
          DirectoryServer
              .getDefaultAttributeType(EntryContainer.ATTR_DEBUG_SEARCH_INDEX);
    }

    if ((refAttrType =
        DirectoryServer.getAttributeType(ATTR_REFERRAL_URL)) == null)
    {
      refAttrType =
          DirectoryServer.getDefaultAttributeType(ATTR_REFERRAL_URL);
    }
    try
    {
      debugSearchIndexDN = DN.decode("cn=debugsearch");
    }
    catch (DirectoryException ex)
    {
      // Should never happen.
    }
  }



  /**
   * The list that holds that ACIs keyed by the DN of the entry holding
   * the ACI.
   */
  private AciList aciList;

  /**
   * The listener that handles ACI changes caused by LDAP operations,
   * ACI decode failure alert logging and backend initialization ACI
   * list adjustment.
   */
  private AciListenerManager aciListenerMgr;



  /**
   * Creates a new DSEE-compatible access control handler.
   */
  public AciHandler()
  {
    // No implementation required. All initialization should be done in
    // the intializeAccessControlHandler method.
  }



  /**
   * {@inheritDoc}
   */
  @Override
  public SearchResultEntry filterEntry(SearchOperation operation,
      SearchResultEntry entry)
  {
    AciLDAPOperationContainer operationContainer =
        new AciLDAPOperationContainer(operation, (ACI_READ), entry);
    // Proxy access check has already been done for this entry in the
    // maySend method, set the seen flag to true to bypass any proxy
    // check.
    operationContainer.setSeenEntry(true);
    SearchResultEntry returnEntry;
    boolean skipCheck = skipAccessCheck(operation);
    if (!skipCheck)
    {
      returnEntry = accessAllowedAttrs(operationContainer);
    }
    else
    {
      returnEntry = entry;
    }
    if (operationContainer.hasGetEffectiveRightsControl())
    {
      returnEntry =
          AciEffectiveRights.addRightsToEntry(this, operation
              .getAttributes(), operationContainer, returnEntry,
              skipCheck);
    }
    return returnEntry;
  }



  /**
   * {@inheritDoc}
   */
  @Override
  public SearchResultEntry filterEntry(Operation operation, Entry entry)
  {
    AciLDAPOperationContainer operationContainer =
        new AciLDAPOperationContainer(operation, (ACI_READ), entry);
    return accessAllowedAttrs(operationContainer);
  }



  /**
   * {@inheritDoc}
   */
  @Override()
  public void finalizeAccessControlHandler()
  {
    aciListenerMgr.finalizeListenerManager();
    AciEffectiveRights.finalizeOnShutdown();
    DirectoryServer
        .deregisterSupportedControl(OID_GET_EFFECTIVE_RIGHTS);
  }



  /**
   * {@inheritDoc}
   */
  @Override()
  public void initializeAccessControlHandler(
      DseeCompatAccessControlHandlerCfg configuration)
      throws ConfigException, InitializationException
  {
    initStatics();
    DN configurationDN = configuration.dn();
    aciList = new AciList(configurationDN);
    aciListenerMgr = new AciListenerManager(aciList, configurationDN);
    processGlobalAcis(configuration);
    processConfigAcis();
    DirectoryServer.registerSupportedControl(OID_GET_EFFECTIVE_RIGHTS);
  }



  /**
   * {@inheritDoc}
   */
  @Override
  public boolean isAllowed(DN entryDN, Operation op, Control control)
      throws DirectoryException
  {
    boolean ret;
    if (!(ret = skipAccessCheck(op)))
    {
      Entry e = new Entry(entryDN, null, null, null);
      AciLDAPOperationContainer operationContainer =
          new AciLDAPOperationContainer(op, e, control,
              (ACI_READ | ACI_CONTROL));
      ret = accessAllowed(operationContainer);
    }
    if (control.getOID().equals(OID_PROXIED_AUTH_V2)
        || control.getOID().equals(OID_PROXIED_AUTH_V1))
    {
      op.setAttachment(ORIG_AUTH_ENTRY, op.getAuthorizationEntry());
    }
    else if (control.getOID().equals(OID_GET_EFFECTIVE_RIGHTS))
    {
      GetEffectiveRightsRequestControl getEffectiveRightsControl;
      if (control instanceof LDAPControl)
      {
        getEffectiveRightsControl =
            GetEffectiveRightsRequestControl.DECODER.decode(control
                .isCritical(), ((LDAPControl) control).getValue());
      }
      else
      {
        getEffectiveRightsControl =
            (GetEffectiveRightsRequestControl) control;
      }
      op.setAttachment(OID_GET_EFFECTIVE_RIGHTS,
          getEffectiveRightsControl);
    }
    return ret;
  }



  /**
   * {@inheritDoc}
   */
  @Override
  public boolean isAllowed(ExtendedOperation operation)
  {
    boolean ret;
    if (!(ret = skipAccessCheck(operation)))
    {
      Entry e =
          new Entry(operation.getAuthorizationDN(), null, null, null);
      AciLDAPOperationContainer operationContainer =
          new AciLDAPOperationContainer(operation, e,
              (ACI_READ | ACI_EXT_OP));
      ret = accessAllowed(operationContainer);
    }
    return ret;
  }



  /**
   * {@inheritDoc}
   */
  @Override
  public boolean isAllowed(LocalBackendAddOperation operation)
      throws DirectoryException
  {
    AciLDAPOperationContainer operationContainer =
        new AciLDAPOperationContainer(operation, ACI_ADD);
    boolean ret = isAllowed(operationContainer, operation);

    // LDAP add needs a verify ACI syntax step in case any
    // "aci" attribute types are being added.
    if (ret)
    {
      ret =
          verifySyntax(operation.getEntryToAdd(), operation,
              operationContainer.getClientDN());
    }
    return ret;
  }



  /**
   * {@inheritDoc}
   */
  @Override
  public boolean isAllowed(LocalBackendBindOperation bindOperation)
  {
    // Not planned to be implemented.
    return true;
  }



  /**
   * Check access on compare operations. Note that the attribute type is
   * unavailable at this time, so this method partially parses the raw
   * attribute string to get the base attribute type. Options are
   * ignored.
   *
   * @param operation
   *          The compare operation to check access on.
   * @return True if access is allowed.
   */
  @Override
  public boolean isAllowed(LocalBackendCompareOperation operation)
  {
    AciLDAPOperationContainer operationContainer =
        new AciLDAPOperationContainer(operation, ACI_COMPARE);
    String baseName;
    String rawAttributeType = operation.getRawAttributeType();
    int semicolonPosition = rawAttributeType.indexOf(';');
    if (semicolonPosition > 0)
    {
      baseName =
          toLowerCase(rawAttributeType.substring(0, semicolonPosition));
    }
    else
    {
      baseName = toLowerCase(rawAttributeType);
    }
    AttributeType attributeType;
    if ((attributeType = DirectoryServer.getAttributeType(baseName)) == null)
    {
      attributeType = DirectoryServer.getDefaultAttributeType(baseName);
    }
    AttributeValue attributeValue =
        AttributeValues.create(attributeType, operation
            .getAssertionValue());
    operationContainer.setCurrentAttributeType(attributeType);
    operationContainer.setCurrentAttributeValue(attributeValue);
    return isAllowed(operationContainer, operation);
  }



  /**
   * Check access on delete operations.
   *
   * @param operation
   *          The delete operation to check access on.
   * @return True if access is allowed.
   */
  @Override
  public boolean isAllowed(LocalBackendDeleteOperation operation)
  {
    AciLDAPOperationContainer operationContainer =
        new AciLDAPOperationContainer(operation, ACI_DELETE);
    return isAllowed(operationContainer, operation);
  }



  /**
   * Checks access on a modifyDN operation.
   *
   * @param operation
   *          The modifyDN operation to check access on.
   * @return True if access is allowed.
   */
  @Override
  public boolean isAllowed(LocalBackendModifyDNOperation operation)
  {
    boolean ret = true;
    DN newSuperiorDN;
    RDN oldRDN = operation.getOriginalEntry().getDN().getRDN();
    RDN newRDN = operation.getNewRDN();
    if (!skipAccessCheck(operation))
    {
      // If this is a modifyDN move to a new superior, then check if the
      // superior DN has import accesss.
      if ((newSuperiorDN = operation.getNewSuperior()) != null)
      {
        try
        {
          ret = aciCheckSuperiorEntry(newSuperiorDN, operation);
        }
        catch (DirectoryException ex)
        {
          ret = false;
        }
      }
      boolean rdnEquals = oldRDN.equals(newRDN);
      // Perform the RDN access checks only if the RDNs are not equal.
      if (ret && !rdnEquals)
      {
        ret = aciCheckRDNs(operation, oldRDN, newRDN);
      }

      // If this is a modifyDN move to a new superior, then check if the
      // original entry DN has export access.
      if (ret && (newSuperiorDN != null))
      {
        AciLDAPOperationContainer operationContainer =
            new AciLDAPOperationContainer(operation, (ACI_EXPORT),
                operation.getOriginalEntry());
        // The RDNs are not equal, skip the proxy check since it was
        // already performed in the aciCheckRDNs call above.
        if (!rdnEquals)
        {
          operationContainer.setSeenEntry(true);
        }
        ret = accessAllowed(operationContainer);
      }
    }
    return ret;
  }



  /**
   * {@inheritDoc}
   */
  @Override
  public boolean isAllowed(LocalBackendModifyOperation operation)
      throws DirectoryException
  {
    AciLDAPOperationContainer operationContainer =
        new AciLDAPOperationContainer(operation, ACI_NULL);
    return aciCheckMods(operationContainer, operation,
        skipAccessCheck(operation));
  }



  /**
   * {@inheritDoc}
   */
  @Override
  public boolean isAllowed(LocalBackendSearchOperation searchOperation)
  {
    // Not planned to be implemented.
    return true;
  }



  /**
   * {@inheritDoc}
   */
  @Override
  public boolean isAllowed(Operation operation, Entry entry,
    SearchFilter filter) throws DirectoryException
  {
    AciLDAPOperationContainer operationContainer =
        new AciLDAPOperationContainer(operation, (ACI_READ), entry);
    return testFilter(operationContainer, filter);
  }



  /**
   * {@inheritDoc}
   */
  @Override
  public boolean mayProxy(Entry proxyUser, Entry proxiedUser,
      Operation op)
  {
    boolean ret;
    if (!(ret = skipAccessCheck(proxyUser)))
    {
      AuthenticationInfo authInfo =
          new AuthenticationInfo(proxyUser, DirectoryServer
              .isRootDN(proxyUser.getDN()));
      AciLDAPOperationContainer operationContainer =
          new AciLDAPOperationContainer(op, proxiedUser, authInfo,
              ACI_PROXY);
      ret = accessAllowedEntry(operationContainer);
    }
    return ret;
  }



  /**
   * {@inheritDoc}
   */
  @Override
  public boolean maySend(DN dn, SearchOperation operation,
      SearchResultReference reference)
  {
    boolean ret;
    if (!(ret = skipAccessCheck(operation)))
    {
      Entry e = new Entry(dn, null, null, null);
      AttributeBuilder builder =
          new AttributeBuilder(refAttrType, ATTR_REFERRAL_URL);
      List<String> URLStrings = reference.getReferralURLs();

      // Load the values, a bind rule might want to evaluate them.
      for (String URLString : URLStrings)
      {
        builder.add(AttributeValues.create(refAttrType, URLString));
      }

      e.addAttribute(builder.toAttribute(), null);
      SearchResultEntry se = new SearchResultEntry(e);
      AciLDAPOperationContainer operationContainer =
          new AciLDAPOperationContainer(operation, (ACI_READ), se);
      operationContainer.setCurrentAttributeType(refAttrType);
      ret = accessAllowed(operationContainer);
    }
    return ret;
  }



  /**
   * Checks access on a search operation.
   *
   * @param operation
   *          The search operation class containing information to check
   *          the access on.
   * @param entry
   *          The entry to evaluate access.
   * @return True if access is allowed.
   */
  @Override
  public boolean maySend(SearchOperation operation,
      SearchResultEntry entry)
  {
    AciLDAPOperationContainer operationContainer =
        new AciLDAPOperationContainer(operation, (ACI_SEARCH), entry);
    boolean ret;
    if (!(ret = skipAccessCheck(operation)))
    {
      try
      {
        ret = testFilter(operationContainer, operation.getFilter());
      }
      catch (DirectoryException ex)
      {
        ret = false;
      }
      if (ret)
      {
        operationContainer.clearEvalAttributes(ACI_NULL);
        operationContainer.setRights(ACI_READ);
        ret = accessAllowedEntry(operationContainer);
        if (ret)
        {
          if (!operationContainer.hasEvalUserAttributes())
          {
            operation.setAttachment(ALL_USER_ATTRS_MATCHED,
                ALL_USER_ATTRS_MATCHED);
          }
          if (!operationContainer.hasEvalOpAttributes())
          {
            operation.setAttachment(ALL_OP_ATTRS_MATCHED,
                ALL_OP_ATTRS_MATCHED);
          }
        }
      }
    }
    // Save a copy of the full resource entry for possible
    // userattr bind rule or geteffectiveright's evaluations in the
    // filterEnty method.
    operation.setAttachment(ALL_ATTRS_RESOURCE_ENTRY, entry);
    return ret;
  }



  /**
   * Check access using the specified container. This container will
   * have all of the information to gather applicable ACIs and perform
   * evaluation on them.
   *
   * @param container
   *          An ACI operation container which has all of the
   *          information needed to check access.
   * @return True if access is allowed.
   */
  boolean accessAllowed(AciContainer container)
  {
    DN dn = container.getResourceEntry().getDN();
    // For ACI_WRITE_ADD and ACI_WRITE_DELETE set the ACI_WRITE
    // right.
    if (container.hasRights(ACI_WRITE_ADD)
        || container.hasRights(ACI_WRITE_DELETE))
    {
      container.setRights(container.getRights() | ACI_WRITE);
    }
    // Check if the ACI_SELF right needs to be set (selfwrite right).
    // Only done if the right is ACI_WRITE, an attribute value is set
    // and that attribute value is a DN.
    if ((container.getCurrentAttributeValue() != null)
        && (container.hasRights(ACI_WRITE))
        && (isAttributeDN(container.getCurrentAttributeType())))
    {
      String DNString = null;
      try
      {
        DNString =
            container.getCurrentAttributeValue().getValue().toString();
        DN tmpDN = DN.decode(DNString);
        // Have a valid DN, compare to clientDN to see if the ACI_SELF
        // right should be set.
        if (tmpDN.equals(container.getClientDN()))
        {
          container.setRights(container.getRights() | ACI_SELF);
        }
      }
      catch (DirectoryException ex)
      {
        // Log a message and keep going.
        Message message = WARN_ACI_NOT_VALID_DN.get(DNString);
        logError(message);
      }
    }

    // Check proxy authorization only if the entry has not already been
    // processed (working on a new entry). If working on a new entry,
    // then only do a proxy check if the right is not set to ACI_PROXY
    // and the proxied authorization control has been decoded.
    if (!container.hasSeenEntry())
    {
      if (container.isProxiedAuthorization()
          && !container.hasRights(ACI_PROXY)
          && !container.hasRights(ACI_SKIP_PROXY_CHECK))
      {
        int currentRights = container.getRights();
        // Save the current rights so they can be put back if on
        // success.
        container.setRights(ACI_PROXY);
        // Switch to the original authorization entry, not the proxied
        // one.
        container.useOrigAuthorizationEntry(true);
        if (!accessAllowed(container))
        {
          return false;
        }
        // Access is ok, put the original rights back.
        container.setRights(currentRights);
        // Put the proxied authorization entry back to the current
        // authorization entry.
        container.useOrigAuthorizationEntry(false);
      }
      // Set the seen flag so proxy processing is not performed for this
      // entry again.
      container.setSeenEntry(true);
    }

    /*
     * First get all allowed candidate ACIs.
     */
    LinkedList<Aci> candidates = aciList.getCandidateAcis(dn);
    /*
     * Create an applicable list of ACIs by target matching each
     * candidate ACI against the container's target match view.
     */
    createApplicableList(candidates, container);
    /*
     * Evaluate the applicable list.
     */
    boolean ret = testApplicableLists(container);
    // Build summary string if doing geteffectiverights eval.
    if (container.isGetEffectiveRightsEval())
    {
      AciEffectiveRights.createSummary(container, ret, "main");
    }
    return ret;
  }



  /*
   * TODO Evaluate performance of this method. TODO Evaluate security
   * concerns of this method. Logic from this method taken almost
   * directly from DS6 implementation. I find the work done in the
   * accessAllowedEntry method, particularly with regard to the entry
   * test evaluation, to be very confusing and potentially pretty
   * inefficient. I'm also concerned that the "return "true" inside the
   * for loop could potentially allow access when it should be denied.
   */

  /**
   * Check if access is allowed on an entry. Access is checked by
   * iterating through each attribute of an entry, starting with the
   * "objectclass" attribute type. If access is allowed on the entry
   * based on one of it's attribute types, then a possible second access
   * check is performed. This second check is only performed if an entry
   * test ACI was found during the earlier successful access check. An
   * entry test ACI has no "targetattrs" keyword, so allowing access
   * based on an attribute type only would be incorrect.
   *
   * @param container
   *          ACI search container containing all of the information
   *          needed to check access.
   * @return True if access is allowed.
   */
  boolean accessAllowedEntry(AciLDAPOperationContainer container)
  {
    boolean ret = false;
    // set flag that specifies this is the first attribute evaluated
    // in the entry
    container.setIsFirstAttribute(true);
    List<AttributeType> typeList =
        getAllAttrs(container.getResourceEntry());
    for (AttributeType attrType : typeList)
    {
      container.setCurrentAttributeType(attrType);
      /*
       * Check if access is allowed. If true, then check to see if an
       * entry test rule was found (no targetattrs) during target match
       * evaluation. If such a rule was found, set the current attribute
       * type to "null" and check access again so that rule is applied.
       */
      if (accessAllowed(container))
      {
        if (container.hasEntryTestRule())
        {
          container.setCurrentAttributeType(null);
          if (!accessAllowed(container))
          {
            /*
             * If we failed because of a deny permission-bind rule, we
             * need to stop and return false.
             */
            if (container.isDenyEval())
            {
              return false;
            }
            /*
             * If we failed because there was no explicit allow rule,
             * then we grant implicit access to the entry.
             */
          }
        }
        return true;
      }
    }
    return ret;
  }



  /**
   * Performs an access check against all of the attributes of an entry.
   * The attributes that fail access are removed from the entry. This
   * method performs the processing needed for the filterEntry method
   * processing.
   *
   * @param container
   *          The search or compare container which has all of the
   *          information needed to filter the attributes for this
   *          entry.
   * @return The entry to send back to the client, minus any attribute
   *         types that failed access check.
   */
  private SearchResultEntry accessAllowedAttrs(
      AciLDAPOperationContainer container)
  {
    Entry e = container.getResourceEntry();
    List<AttributeType> typeList = getAllAttrs(e);
    for (AttributeType attrType : typeList)
    {
      if (container.hasAllUserAttributes() && !attrType.isOperational())
      {
        continue;
      }
      if (container.hasAllOpAttributes() && attrType.isOperational())
      {
        continue;
      }
      container.setCurrentAttributeType(attrType);
      if (!accessAllowed(container))
      {
        e.removeAttribute(attrType);
      }
    }
    return container.getSearchResultEntry();
  }



  /**
   * Checks to see if a LDAP modification is allowed access.
   *
   * @param container
   *          The structure containing the LDAP modifications
   * @param operation
   *          The operation to check modify privileges on. operation to
   *          check and the evaluation context to apply the check
   *          against.
   * @param skipAccessCheck
   *          True if access checking should be skipped.
   * @return True if access is allowed.
   * @throws DirectoryException
   *           If a modified ACI could not be decoded.
   */
  private boolean aciCheckMods(AciLDAPOperationContainer container,
      LocalBackendModifyOperation operation, boolean skipAccessCheck)
      throws DirectoryException
  {
    Entry resourceEntry = container.getResourceEntry();
    DN dn = resourceEntry.getDN();
    List<Modification> modifications = container.getModifications();
    for (Modification m : modifications)
    {
      Attribute modAttr = m.getAttribute();
      AttributeType modAttrType = modAttr.getAttributeType();

      if (modAttrType.equals(aciType))
      {
        /*
         * Check that the operation has modify privileges if it contains
         * an "aci" attribute type.
         */
        if (!operation.getClientConnection().hasPrivilege(
            Privilege.MODIFY_ACL, operation))
        {
          Message message =
              INFO_ACI_MODIFY_FAILED_PRIVILEGE.get(String
                  .valueOf(container.getResourceDN()), String
                  .valueOf(container.getClientDN()));
          logError(message);
          return false;
        }
      }
      // This access check handles the case where all attributes of this
      // type are being replaced or deleted. If only a subset is being
      // deleted than this access check is skipped.
      ModificationType modType = m.getModificationType();
      if (((modType == ModificationType.DELETE) && modAttr.isEmpty())
          || ((modType == ModificationType.REPLACE)
              || (modType == ModificationType.INCREMENT)))
      {
        /*
         * Check if we have rights to delete all values of an attribute
         * type in the resource entry.
         */
        if (resourceEntry.hasAttribute(modAttrType))
        {
          container.setCurrentAttributeType(modAttrType);
          List<Attribute> attrList =
              resourceEntry.getAttribute(modAttrType, modAttr
                  .getOptions());
          if (attrList != null)
          {
            for (Attribute a : attrList)
            {
              for (AttributeValue v : a)
              {
                container.setCurrentAttributeValue(v);
                container.setRights(ACI_WRITE_DELETE);
                if (!skipAccessCheck && !accessAllowed(container))
                {
                  return false;
                }
              }
            }
          }
        }
      }

      if (!modAttr.isEmpty())
      {
        for (AttributeValue v : modAttr)
        {
          container.setCurrentAttributeType(modAttrType);
          switch (m.getModificationType())
          {
          case ADD:
          case REPLACE:
            container.setCurrentAttributeValue(v);
            container.setRights(ACI_WRITE_ADD);
            if (!skipAccessCheck && !accessAllowed(container))
            {
              return false;
            }
            break;
          case DELETE:
            container.setCurrentAttributeValue(v);
            container.setRights(ACI_WRITE_DELETE);
            if (!skipAccessCheck && !accessAllowed(container))
            {
              return false;
            }
            break;
          case INCREMENT:
            Entry modifiedEntry = operation.getModifiedEntry();
            List<Attribute> modifiedAttrs =
                modifiedEntry.getAttribute(modAttrType, modAttr
                    .getOptions());
            if (modifiedAttrs != null)
            {
              for (Attribute attr : modifiedAttrs)
              {
                for (AttributeValue val : attr)
                {
                  container.setCurrentAttributeValue(val);
                  container.setRights(ACI_WRITE_ADD);
                  if (!skipAccessCheck && !accessAllowed(container))
                  {
                    return false;
                  }
                }
              }
            }
            break;
          }
          /*
           * Check if the modification type has an "aci" attribute type.
           * If so, check the syntax of that attribute value. Fail the
           * the operation if the syntax check fails.
           */
          if (modAttrType.equals(aciType)
              || modAttrType.equals(globalAciType))
          {
            try
            {
              // A global ACI needs a NULL DN, not the DN of the
              // modification.
              if (modAttrType.equals(globalAciType))
              {
                dn = DN.nullDN();
              }
              Aci.decode(v.getValue(), dn);
            }
            catch (AciException ex)
            {
              Message message =
                  WARN_ACI_MODIFY_FAILED_DECODE.get(String.valueOf(dn),
                      ex.getMessage());
              throw new DirectoryException(
                  ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
            }
          }
        }
      }
    }
    return true;
  }



  /**
   * Perform all needed RDN checks for the modifyDN operation. The old
   * RDN is not equal to the new RDN. The access checks are: - Verify
   * WRITE access to the original entry. - Verfiy WRITE_ADD access on
   * each RDN component of the new RDN. The WRITE_ADD access is used
   * because this access could be restricted by the targattrfilters
   * keyword. - If the deleteOLDRDN flag is set, verify WRITE_DELETE
   * access on the old RDN. The WRITE_DELETE access is used because this
   * access could be restricted by the targattrfilters keyword.
   *
   * @param operation
   *          The ModifyDN operation class containing information to
   *          check access on.
   * @param oldRDN
   *          The old RDN component.
   * @param newRDN
   *          The new RDN component.
   * @return True if access is allowed.
   */
  private boolean aciCheckRDNs(LocalBackendModifyDNOperation operation,
      RDN oldRDN, RDN newRDN)
  {
    boolean ret;

    AciLDAPOperationContainer operationContainer =
        new AciLDAPOperationContainer(operation, (ACI_WRITE), operation
            .getOriginalEntry());
    ret = accessAllowed(operationContainer);
    if (ret)
    {
      ret = checkRDN(ACI_WRITE_ADD, newRDN, operationContainer);
    }
    if (ret && operation.deleteOldRDN())
    {
      ret = checkRDN(ACI_WRITE_DELETE, oldRDN, operationContainer);
    }
    return ret;
  }



  /**
   * Check access on the new superior entry if it exists. If the entry
   * does not exist or the DN cannot be locked then false is returned.
   *
   * @param superiorDN
   *          The DN of the new superior entry.
   * @param op
   *          The modifyDN operation to check access on.
   * @return True if access is granted to the new superior entry.
   * @throws DirectoryException
   *           If a problem occurs while trying to retrieve the new
   *           superior entry.
   */
  private boolean aciCheckSuperiorEntry(DN superiorDN,
      LocalBackendModifyDNOperation op) throws DirectoryException
  {
    boolean ret = false;
    Lock entryLock = null;
    for (int i = 0; i < 3; i++)
    {
      entryLock = LockManager.lockRead(superiorDN);
      if (entryLock != null)
      {
        break;
      }
    }
    if (entryLock == null)
    {
      Message message =
          WARN_ACI_HANDLER_CANNOT_LOCK_NEW_SUPERIOR_USER.get(String
              .valueOf(superiorDN));
      logError(message);
      return false;
    }
    try
    {
      Entry superiorEntry = DirectoryServer.getEntry(superiorDN);
      if (superiorEntry != null)
      {
        AciLDAPOperationContainer operationContainer =
            new AciLDAPOperationContainer(op, (ACI_IMPORT),
                superiorEntry);
        ret = accessAllowed(operationContainer);
      }
    }
    finally
    {
      LockManager.unlock(superiorDN, entryLock);
    }
    return ret;
  }



  /**
   * Check access on each attribute-value pair component of the
   * specified RDN. There may be more than one attribute-value pair if
   * the RDN is multi-valued.
   *
   * @param right
   *          The access right to check for.
   * @param rdn
   *          The RDN to examine the attribute-value pairs of.
   * @param container
   *          The container containing the information needed to
   *          evaluate the specified RDN.
   * @return True if access is allowed for all attribute-value pairs.
   */
  private boolean checkRDN(int right, RDN rdn, AciContainer container)
  {
    boolean ret = false;
    int numAVAs = rdn.getNumValues();
    container.setRights(right);
    for (int i = 0; i < numAVAs; i++)
    {
      AttributeType type = rdn.getAttributeType(i);
      AttributeValue value = rdn.getAttributeValue(i);
      container.setCurrentAttributeType(type);
      container.setCurrentAttributeValue(value);
      if (!(ret = accessAllowed(container)))
      {
        break;
      }
    }
    return ret;
  }



  /**
   * Creates the allow and deny ACI lists based on the provided target
   * match context. These lists are stored in the evaluation context.
   *
   * @param candidates
   *          List of all possible ACI candidates.
   * @param targetMatchCtx
   *          Target matching context to use for testing each ACI.
   */
  private void createApplicableList(LinkedList<Aci> candidates,
      AciTargetMatchContext targetMatchCtx)
  {
    LinkedList<Aci> denys = new LinkedList<Aci>();
    LinkedList<Aci> allows = new LinkedList<Aci>();
    for (Aci aci : candidates)
    {
      if (Aci.isApplicable(aci, targetMatchCtx))
      {
        if (aci.hasAccessType(EnumAccessType.DENY))
        {
          denys.add(aci);
        }
        if (aci.hasAccessType(EnumAccessType.ALLOW))
        {
          allows.add(aci);
        }
      }
      if (targetMatchCtx.getTargAttrFiltersMatch())
      {
        targetMatchCtx.setTargAttrFiltersMatch(false);
      }
    }
    targetMatchCtx.setAllowList(allows);
    targetMatchCtx.setDenyList(denys);
  }



  /**
   * Gathers all of the attribute types in an entry along with the
   * "objectclass" attribute type in a List. The "objectclass" attribute
   * is added to the list first so it is evaluated first.
   *
   * @param e
   *          Entry to gather the attributes for.
   * @return List containing the attribute types.
   */
  private List<AttributeType> getAllAttrs(Entry e)
  {
    Map<AttributeType, List<Attribute>> attrMap = e.getUserAttributes();
    Map<AttributeType, List<Attribute>> opAttrMap =
        e.getOperationalAttributes();
    List<AttributeType> typeList = new LinkedList<AttributeType>();
    Attribute attr = e.getObjectClassAttribute();
    /*
     * When a search is not all attributes returned, the "objectclass"
     * attribute type is missing from the entry.
     */
    if (attr != null)
    {
      AttributeType ocType = attr.getAttributeType();
      typeList.add(ocType);
    }
    typeList.addAll(attrMap.keySet());
    typeList.addAll(opAttrMap.keySet());
    return typeList;
  }



  /**
   * Check access using the accessAllowed method. The LDAP add, compare,
   * modify and delete operations use this function. The other supported
   * LDAP operations have more specialized checks.
   *
   * @param operationContainer
   *          The container containing the information needed to
   *          evaluate this operation.
   * @param operation
   *          The operation being evaluated.
   * @return True if this operation is allowed access.
   */
  private boolean isAllowed(
      AciLDAPOperationContainer operationContainer, Operation operation)
  {
    return skipAccessCheck(operation)
        || accessAllowed(operationContainer);
  }



  /**
   * Check if the specified attribute type is a DN by checking if its
   * syntax OID is equal to the DN syntax OID.
   *
   * @param attribute
   *          The attribute type to check.
   * @return True if the attribute type syntax OID is equal to a DN
   *         syntax OID.
   */
  private boolean isAttributeDN(AttributeType attribute)
  {
    return (attribute.getSyntaxOID().equals(SYNTAX_DN_OID));
  }



  /**
   * Process all ACIs under the "cn=config" naming context and adds them
   * to the ACI list cache. It also logs messages about the number of
   * ACIs added to the cache. This method is called once at startup. It
   * will put the server in lockdown mode if needed.
   *
   * @throws InitializationException
   *           If there is an error searching for the ACIs in the naming
   *           context.
   */
  private void processConfigAcis() throws InitializationException
  {
    try
    {
      DN configDN = DN.decode("cn=config");
      LinkedHashSet<String> attrs = new LinkedHashSet<String>(1);
      attrs.add("aci");
      LinkedList<Message> failedACIMsgs = new LinkedList<Message>();
      InternalClientConnection conn =
          InternalClientConnection.getRootConnection();
      InternalSearchOperation op =
          conn.processSearch(configDN, SearchScope.WHOLE_SUBTREE,
              DereferencePolicy.NEVER_DEREF_ALIASES, 0, 0, false,
              SearchFilter.createFilterFromString("aci=*"), attrs);
      if (!op.getSearchEntries().isEmpty())
      {
        int validAcis =
            aciList.addAci(op.getSearchEntries(), failedACIMsgs);
        if (!failedACIMsgs.isEmpty())
        {
          aciListenerMgr.logMsgsSetLockDownMode(failedACIMsgs);
        }
        Message message =
            INFO_ACI_ADD_LIST_ACIS.get(Integer.toString(validAcis),
                String.valueOf(configDN));
        logError(message);
      }
    }
    catch (DirectoryException e)
    {
      Message message = INFO_ACI_HANDLER_FAIL_PROCESS_ACI.get();
      throw new InitializationException(message, e);
    }
  }



  /**
   * Process all global ACI attribute types found in the configuration
   * entry and adds them to that ACI list cache. It also logs messages
   * about the number of ACI attribute types added to the cache. This
   * method is called once at startup. It also will put the server into
   * lockdown mode if needed.
   *
   * @param configuration
   *          The config handler containing the ACI configuration
   *          information.
   * @throws InitializationException
   *           If there is an error reading the global ACIs from the
   *           configuration entry.
   */
  private void processGlobalAcis(
      DseeCompatAccessControlHandlerCfg configuration)
      throws InitializationException
  {
    SortedSet<Aci> globalAcis = configuration.getGlobalACI();
    try
    {
      if (globalAcis != null)
      {
        aciList.addAci(DN.nullDN(), globalAcis);
        Message message =
            INFO_ACI_ADD_LIST_GLOBAL_ACIS.get(Integer
                .toString(globalAcis.size()));
        logError(message);
      }
    }
    catch (Exception e)
    {
      if (debugEnabled())
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }
      Message message =
          INFO_ACI_HANDLER_FAIL_PROCESS_GLOBAL_ACI.get(String
              .valueOf(configuration.dn()));
      throw new InitializationException(message, e);
    }
  }



  /**
   * Check to see if the specified entry has the specified privilege.
   *
   * @param e
   *          The entry to check privileges on.
   * @return {@code true} if the entry has the specified privilege, or
   *         {@code false} if not.
   */
  private boolean skipAccessCheck(Entry e)
  {
    return ClientConnection.hasPrivilege(e, Privilege.BYPASS_ACL);
  }



  /**
   * Check to see if the client entry has BYPASS_ACL privileges for this
   * operation.
   *
   * @param operation
   *          The operation to check privileges on.
   * @return True if access checking can be skipped because the
   *         operation client connection has BYPASS_ACL privileges.
   */
  private boolean skipAccessCheck(Operation operation)
  {
    return operation.getClientConnection().hasPrivilege(
        Privilege.BYPASS_ACL, operation);
  }



  /**
   * Performs the test of the deny and allow access lists using the
   * provided evaluation context. The deny list is checked first.
   *
   * @param evalCtx
   *          The evaluation context to use.
   * @return True if access is allowed.
   */
  private boolean testApplicableLists(AciEvalContext evalCtx)
  {
    EnumEvalResult res;
    evalCtx.setEvalReason(EnumEvalReason.NO_REASON);
    LinkedList<Aci> denys = evalCtx.getDenyList();
    LinkedList<Aci> allows = evalCtx.getAllowList();
    // If allows list is empty and not doing geteffectiverights return
    // false.
    evalCtx.setDenyEval(true);
    if (allows.isEmpty()
        && !(evalCtx.isGetEffectiveRightsEval()
            && !evalCtx.hasRights(ACI_SELF) && evalCtx
            .isTargAttrFilterMatchAciEmpty()))
    {
      evalCtx.setEvalReason(EnumEvalReason.NO_ALLOW_ACIS);
      evalCtx.setDecidingAci(null);
      return false;
    }
    for (Aci denyAci : denys)
    {
      res = Aci.evaluate(evalCtx, denyAci);
      // Failure could be returned if a system limit is hit or
      // search fails
      if (res.equals(EnumEvalResult.FAIL))
      {
        evalCtx.setEvalReason(EnumEvalReason.EVALUATED_DENY_ACI);
        evalCtx.setDecidingAci(denyAci);
        return false;
      }
      else if (res.equals(EnumEvalResult.TRUE))
      {
        if (evalCtx.isGetEffectiveRightsEval()
            && !evalCtx.hasRights(ACI_SELF)
            && !evalCtx.isTargAttrFilterMatchAciEmpty())
        {
          // Iterate to next only if deny ACI contains a targattrfilters
          // keyword.
          if (AciEffectiveRights.setTargAttrAci(evalCtx, denyAci, true))
          {
            continue;
          }
          evalCtx.setEvalReason(EnumEvalReason.EVALUATED_DENY_ACI);
          evalCtx.setDecidingAci(denyAci);
          return false;
        }
        else
        {
          evalCtx.setEvalReason(EnumEvalReason.EVALUATED_DENY_ACI);
          evalCtx.setDecidingAci(denyAci);
          return false;
        }
      }
    }
    // Now check the allows -- flip the deny flag to false first.
    evalCtx.setDenyEval(false);
    for (Aci allowAci : allows)
    {
      res = Aci.evaluate(evalCtx, allowAci);
      if (res.equals(EnumEvalResult.TRUE))
      {
        if (evalCtx.isGetEffectiveRightsEval()
            && !evalCtx.hasRights(ACI_SELF)
            && !evalCtx.isTargAttrFilterMatchAciEmpty())
        {
          // Iterate to next only if deny ACI contains a targattrfilters
          // keyword.
          if (AciEffectiveRights.setTargAttrAci(evalCtx, allowAci,
              false))
          {
            continue;
          }
          evalCtx.setEvalReason(EnumEvalReason.EVALUATED_ALLOW_ACI);
          evalCtx.setDecidingAci(allowAci);
          return true;
        }
        else
        {
          evalCtx.setEvalReason(EnumEvalReason.EVALUATED_ALLOW_ACI);
          evalCtx.setDecidingAci(allowAci);
          return true;
        }
      }
    }
    // Nothing matched fall through.
    evalCtx.setEvalReason(EnumEvalReason.NO_MATCHED_ALLOWS_ACIS);
    evalCtx.setDecidingAci(null);
    return false;
  }



  /**
   * Test the attribute types of the search filter for access. This
   * method supports the search right.
   *
   * @param container
   *          The container used in the access evaluation.
   * @param filter
   *          The filter to check access on.
   * @return True if all attribute types in the filter have access.
   * @throws DirectoryException
   *           If there is a problem matching the entry using the
   *           provided filter.
   */
  private boolean testFilter(AciLDAPOperationContainer container,
      SearchFilter filter) throws DirectoryException
  {
    boolean ret = true;
    // If the resource entry has a dn equal to "cn=debugsearch" and it
    // contains the special attribute type "debugsearchindex", then the
    // resource entry is a pseudo entry created for debug purposes.
    // Return true if that is the case.
    if (debugSearchIndexDN.equals(container.getResourceDN())
        && container.getResourceEntry().hasAttribute(debugSearchIndex))
    {
      return true;
    }
    switch (filter.getFilterType())
    {
    case AND:
    case OR:
    {
      for (SearchFilter f : filter.getFilterComponents())
      {
        if (!testFilter(container, f))
        {
          return false;
        }
      }
      break;
    }
    case NOT:
    {
      SearchFilter f = filter.getNotComponent();
      ret = testFilter(container, f);
      break;
    }
    default:
    {
      AttributeType attrType = filter.getAttributeType();
      container.setCurrentAttributeType(attrType);
      ret = accessAllowed(container);
    }
    }
    return ret;
  }



  /**
   * Evaluate an entry to be added to see if it has any "aci" attribute
   * type. If it does, examines each "aci" attribute type value for
   * syntax errors. All of the "aci" attribute type values must pass
   * syntax check for the add operation to proceed. Any entry with an
   * "aci" attribute type must have "modify-acl" privileges.
   *
   * @param entry
   *          The entry to be examined.
   * @param operation
   *          The operation to to check privileges on.
   * @param clientDN
   *          The authorization DN.
   * @return True if the entry has no ACI attributes or if all of the
   *         "aci" attributes values pass ACI syntax checking.
   * @throws DirectoryException
   *           If a modified ACI could not be decoded.
   */
  private boolean verifySyntax(Entry entry, Operation operation,
      DN clientDN) throws DirectoryException
  {
    if (entry.hasOperationalAttribute(aciType))
    {
      /*
       * Check that the operation has "modify-acl" privileges since the
       * entry to be added has an "aci" attribute type.
       */
      if (!operation.getClientConnection().hasPrivilege(
          Privilege.MODIFY_ACL, operation))
      {
        Message message =
            INFO_ACI_ADD_FAILED_PRIVILEGE.get(String.valueOf(entry
                .getDN()), String.valueOf(clientDN));
        logError(message);
        return false;
      }
      List<Attribute> attributeList =
          entry.getOperationalAttribute(aciType, null);
      for (Attribute attribute : attributeList)
      {
        for (AttributeValue value : attribute)
        {
          try
          {
            DN dn = entry.getDN();
            Aci.decode(value.getValue(), dn);
          }
          catch (AciException ex)
          {
            Message message =
                WARN_ACI_ADD_FAILED_DECODE.get(String.valueOf(entry
                    .getDN()), ex.getMessage());
            throw new DirectoryException(
                ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
          }
        }
      }
    }
    return true;
  }
}
