/*
 * 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 legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * 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 legal-notices/CDDLv1_0.txt.
 * 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.
 *      Portions Copyright 2011-2015 ForgeRock AS
 */
package org.opends.server.authorization.dseecompat;

import java.util.*;

import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.SearchScope;
import org.opends.server.api.AlertGenerator;
import org.opends.server.api.Backend;
import org.opends.server.api.BackendInitializationListener;
import org.opends.server.api.plugin.InternalDirectoryServerPlugin;
import org.opends.server.api.plugin.PluginResult;
import org.opends.server.api.plugin.PluginResult.PostOperation;
import org.opends.server.api.plugin.PluginType;
import org.opends.server.core.DirectoryServer;
import org.opends.server.protocols.internal.InternalClientConnection;
import org.opends.server.protocols.internal.InternalSearchOperation;
import org.opends.server.protocols.internal.SearchRequest;
import org.opends.server.protocols.ldap.LDAPControl;
import org.opends.server.types.*;
import org.opends.server.types.operation.*;
import org.opends.server.workflowelement.localbackend.LocalBackendSearchOperation;

import static org.opends.messages.AccessControlMessages.*;
import static org.opends.server.protocols.internal.InternalClientConnection.*;
import static org.opends.server.protocols.internal.Requests.*;
import static org.opends.server.util.ServerConstants.*;

/**
 * The AciListenerManager updates an ACI list after each modification
 * operation. Also, updates ACI list when backends are initialized and
 * finalized.
 */
public class AciListenerManager implements
    BackendInitializationListener, AlertGenerator
{
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();

  /**
   * The fully-qualified name of this class.
   */
  private static final String CLASS_NAME =
      "org.opends.server.authorization.dseecompat.AciListenerManager";



  /**
   * Internal plugin used for updating the cache before a response is
   * sent to the client.
   */
  private final class AciChangeListenerPlugin extends
      InternalDirectoryServerPlugin
  {
    private AciChangeListenerPlugin()
    {
      super(configurationDN, EnumSet.of(
          PluginType.POST_SYNCHRONIZATION_ADD,
          PluginType.POST_SYNCHRONIZATION_DELETE,
          PluginType.POST_SYNCHRONIZATION_MODIFY,
          PluginType.POST_SYNCHRONIZATION_MODIFY_DN,
          PluginType.POST_OPERATION_ADD,
          PluginType.POST_OPERATION_DELETE,
          PluginType.POST_OPERATION_MODIFY,
          PluginType.POST_OPERATION_MODIFY_DN), true);
    }



    /** {@inheritDoc} */
    @Override
    public void doPostSynchronization(
        PostSynchronizationAddOperation addOperation)
    {
      Entry entry = addOperation.getEntryToAdd();
      if (entry != null)
      {
        doPostAdd(entry);
      }
    }



    /** {@inheritDoc} */
    @Override
    public void doPostSynchronization(
        PostSynchronizationDeleteOperation deleteOperation)
    {
      Entry entry = deleteOperation.getEntryToDelete();
      if (entry != null)
      {
        doPostDelete(entry);
      }
    }



    /** {@inheritDoc} */
    @Override
    public void doPostSynchronization(
        PostSynchronizationModifyDNOperation modifyDNOperation)
    {
      Entry entry = modifyDNOperation.getUpdatedEntry();
      if (entry != null)
      {
        doPostModifyDN(entry.getName(), entry.getName());
      }
    }



    /** {@inheritDoc} */
    @Override
    public void doPostSynchronization(
        PostSynchronizationModifyOperation modifyOperation)
    {
      Entry entry = modifyOperation.getCurrentEntry();
      Entry modEntry = modifyOperation.getModifiedEntry();
      if (entry != null && modEntry != null)
      {
        doPostModify(modifyOperation.getModifications(), entry, modEntry);
      }
    }



    /** {@inheritDoc} */
    @Override
    public PostOperation doPostOperation(
        PostOperationAddOperation addOperation)
    {
      // Only do something if the operation is successful, meaning there
      // has been a change.
      if (addOperation.getResultCode() == ResultCode.SUCCESS)
      {
        doPostAdd(addOperation.getEntryToAdd());
      }

      // If we've gotten here, then everything is acceptable.
      return PluginResult.PostOperation.continueOperationProcessing();
    }



    /** {@inheritDoc} */
    @Override
    public PostOperation doPostOperation(
        PostOperationDeleteOperation deleteOperation)
    {
      // Only do something if the operation is successful, meaning there
      // has been a change.
      if (deleteOperation.getResultCode() == ResultCode.SUCCESS)
      {
        doPostDelete(deleteOperation.getEntryToDelete());
      }

      // If we've gotten here, then everything is acceptable.
      return PluginResult.PostOperation.continueOperationProcessing();
    }



    /** {@inheritDoc} */
    @Override
    public PostOperation doPostOperation(
        PostOperationModifyDNOperation modifyDNOperation)
    {
      // Only do something if the operation is successful, meaning there
      // has been a change.
      if (modifyDNOperation.getResultCode() == ResultCode.SUCCESS)
      {
        doPostModifyDN(modifyDNOperation.getOriginalEntry().getName(),
          modifyDNOperation.getUpdatedEntry().getName());
      }

      // If we've gotten here, then everything is acceptable.
      return PluginResult.PostOperation.continueOperationProcessing();
    }



    /** {@inheritDoc} */
    @Override
    public PostOperation doPostOperation(
        PostOperationModifyOperation modifyOperation)
    {
      // Only do something if the operation is successful, meaning there
      // has been a change.
      if (modifyOperation.getResultCode() == ResultCode.SUCCESS)
      {
        doPostModify(modifyOperation.getModifications(), modifyOperation
          .getCurrentEntry(), modifyOperation.getModifiedEntry());
      }

      // If we've gotten here, then everything is acceptable.
      return PluginResult.PostOperation.continueOperationProcessing();
    }



    private void doPostAdd(Entry addedEntry)
    {
      // This entry might have both global and aci attribute types.
      boolean hasAci = addedEntry.hasOperationalAttribute(AciHandler.aciType);
      boolean hasGlobalAci = addedEntry.hasAttribute(AciHandler.globalAciType);
      if (hasAci || hasGlobalAci)
      {
        // Ignore this list, the ACI syntax has already passed and it
        // should be empty.
        List<LocalizableMessage> failedACIMsgs = new LinkedList<>();

        aciList.addAci(addedEntry, hasAci, hasGlobalAci, failedACIMsgs);
      }
    }



    private void doPostDelete(Entry deletedEntry)
    {
      // This entry might have both global and aci attribute types.
      boolean hasAci = deletedEntry.hasOperationalAttribute(
              AciHandler.aciType);
      boolean hasGlobalAci = deletedEntry.hasAttribute(
              AciHandler.globalAciType);
      aciList.removeAci(deletedEntry, hasAci, hasGlobalAci);
    }



    private void doPostModifyDN(DN fromDN, DN toDN)
    {
      aciList.renameAci(fromDN, toDN);
    }



    private void doPostModify(List<Modification> mods, Entry oldEntry,
        Entry newEntry)
    {
      // A change to the ACI list is expensive so let's first make sure
      // that the modification included changes to the ACI. We'll check
      // for both "aci" attribute types and global "ds-cfg-global-aci"
      // attribute types.
      boolean hasAci = false, hasGlobalAci = false;
      for (Modification mod : mods)
      {
        AttributeType attributeType = mod.getAttribute()
            .getAttributeType();
        if (attributeType.equals(AciHandler.aciType))
        {
          hasAci = true;
        }
        else if (attributeType.equals(AciHandler.globalAciType))
        {
          hasGlobalAci = true;
        }

        if (hasAci && hasGlobalAci)
        {
          break;
        }
      }

      if (hasAci || hasGlobalAci)
      {
        aciList.modAciOldNewEntry(oldEntry, newEntry, hasAci,
            hasGlobalAci);
      }
    }

  }



  /** The configuration DN. */
  private DN configurationDN;

  /** True if the server is in lockdown mode. */
  private boolean inLockDownMode;

  /** The AciList caches the ACIs. */
  private AciList aciList;

  /** Search filter used in context search for "aci" attribute types. */
  private static SearchFilter aciFilter;

  /**
   * Internal plugin used for updating the cache before a response is
   * sent to the client.
   */
  private final AciChangeListenerPlugin plugin;

  /** The aci attribute type is operational so we need to specify it to be returned. */
  private static LinkedHashSet<String> attrs = new LinkedHashSet<>();
  static
  {
    // Set up the filter used to search private and public contexts.
    try
    {
      aciFilter = SearchFilter.createFilterFromString("(aci=*)");
    }
    catch (DirectoryException ex)
    {
      // TODO should never happen, error message?
    }
    attrs.add("aci");
  }



  /**
   * Save the list created by the AciHandler routine. Registers as an
   * Alert Generator that can send alerts when the server is being put
   * in lockdown mode. Registers as backend initialization listener that
   * is used to manage the ACI list cache when backends are
   * initialized/finalized. Registers as a change notification listener
   * that is used to manage the ACI list cache after ACI modifications
   * have been performed.
   *
   * @param aciList
   *          The list object created and loaded by the handler.
   * @param cfgDN
   *          The DN of the access control configuration entry.
   */
  public AciListenerManager(AciList aciList, DN cfgDN)
  {
    this.aciList = aciList;
    this.configurationDN = cfgDN;
    this.plugin = new AciChangeListenerPlugin();

    // Process ACI from already registered backends.
    Map<String, Backend> backendMap = DirectoryServer.getBackends();
    if (backendMap != null) {
      for (Backend backend : backendMap.values()) {
        performBackendPreInitializationProcessing(backend);
      }
    }

    DirectoryServer.registerInternalPlugin(plugin);
    DirectoryServer.registerBackendInitializationListener(this);
    DirectoryServer.registerAlertGenerator(this);
  }



  /**
   * Deregister from the change notification listener, the backend
   * initialization listener and the alert generator.
   */
  public void finalizeListenerManager()
  {
    DirectoryServer.deregisterInternalPlugin(plugin);
    DirectoryServer.deregisterBackendInitializationListener(this);
    DirectoryServer.deregisterAlertGenerator(this);
  }



  /**
   * {@inheritDoc} In this case, the server will search the backend to
   * find all aci attribute type values that it may contain and add them
   * to the ACI list.
   */
  @Override
  public void performBackendPreInitializationProcessing(Backend<?> backend)
  {
    // Check to make sure that the backend has a presence index defined
    // for the ACI attribute. If it does not, then log a warning message
    // because this processing could be very expensive.
    AttributeType aciType = DirectoryServer.getAttributeTypeOrDefault("aci");
    if (backend.getEntryCount() > 0
        && !backend.isIndexed(aciType, IndexType.PRESENCE))
    {
      logger.warn(WARN_ACI_ATTRIBUTE_NOT_INDEXED, backend.getBackendID(), "aci");
    }

    LinkedList<LocalizableMessage> failedACIMsgs = new LinkedList<>();

    InternalClientConnection conn = getRootConnection();
    // Add manageDsaIT control so any ACIs in referral entries will be
    // picked up.
    LDAPControl c1 = new LDAPControl(OID_MANAGE_DSAIT_CONTROL, true);
    // Add group membership control to let a backend look for it and
    // decide if it would abort searches.
    LDAPControl c2 = new LDAPControl(OID_INTERNAL_GROUP_MEMBERSHIP_UPDATE, false);

    for (DN baseDN : backend.getBaseDNs())
    {
      try
      {
        if (!backend.entryExists(baseDN))
        {
          continue;
        }
      }
      catch (Exception e)
      {
        logger.traceException(e);
        continue;
      }
      SearchRequest request = newSearchRequest(baseDN, SearchScope.WHOLE_SUBTREE, aciFilter)
          .addControl(c1)
          .addControl(c2)
          .addAttribute(attrs);
      InternalSearchOperation internalSearch =
          new InternalSearchOperation(conn, nextOperationID(), nextMessageID(), request);
      LocalBackendSearchOperation localInternalSearch =
          new LocalBackendSearchOperation(internalSearch);
      try
      {
        backend.search(localInternalSearch);
      }
      catch (Exception e)
      {
        logger.traceException(e);
        continue;
      }
      if (!internalSearch.getSearchEntries().isEmpty())
      {
        int validAcis = aciList.addAci(internalSearch.getSearchEntries(), failedACIMsgs);
        if (!failedACIMsgs.isEmpty())
        {
          logMsgsSetLockDownMode(failedACIMsgs);
        }
        logger.debug(INFO_ACI_ADD_LIST_ACIS, validAcis, baseDN);
      }
    }
  }



  /**
   * {@inheritDoc} In this case, the server will remove all aci
   * attribute type values associated with entries in the provided
   * backend.
   */
  @Override
  public void performBackendPostFinalizationProcessing(Backend<?> backend)
  {
    aciList.removeAci(backend);
  }

  @Override
  public void performBackendPostInitializationProcessing(Backend<?> backend) {
    // Nothing to do.
  }

  @Override
  public void performBackendPreFinalizationProcessing(Backend<?> backend) {
    // nothing to do.
  }


  /**
   * Retrieves the fully-qualified name of the Java class for this alert
   * generator implementation.
   *
   * @return The fully-qualified name of the Java class for this alert
   *         generator implementation.
   */
  @Override
  public String getClassName()
  {
    return CLASS_NAME;
  }



  /**
   * Retrieves the DN of the configuration entry used to configure the
   * handler.
   *
   * @return The DN of the configuration entry containing the Access
   *         Control configuration information.
   */
  @Override
  public DN getComponentEntryDN()
  {
    return this.configurationDN;
  }



  /**
   * Retrieves information about the set of alerts that this generator
   * may produce. The map returned should be between the notification
   * type for a particular notification and the human-readable
   * description for that notification. This alert generator must not
   * generate any alerts with types that are not contained in this list.
   *
   * @return Information about the set of alerts that this generator may
   *         produce.
   */
  @Override
  public LinkedHashMap<String, String> getAlerts()
  {
    LinkedHashMap<String, String> alerts = new LinkedHashMap<>();
    alerts.put(ALERT_TYPE_ACCESS_CONTROL_PARSE_FAILED,
        ALERT_DESCRIPTION_ACCESS_CONTROL_PARSE_FAILED);
    return alerts;
  }

  /**
   * Log the exception messages from the failed ACI decode and then put
   * the server in lockdown mode -- if needed.
   *
   * @param failedACIMsgs
   *          List of exception messages from failed ACI decodes.
   */
  public void logMsgsSetLockDownMode(LinkedList<LocalizableMessage> failedACIMsgs)
  {
    for (LocalizableMessage msg : failedACIMsgs)
    {
      logger.warn(WARN_ACI_SERVER_DECODE_FAILED, msg);
    }
    if (!inLockDownMode)
    {
      setLockDownMode();
    }
  }



  /**
   * Send an WARN_ACI_ENTER_LOCKDOWN_MODE alert notification and put the
   * server in lockdown mode.
   */
  private void setLockDownMode()
  {
    if (!inLockDownMode)
    {
      inLockDownMode = true;
      // Send ALERT_TYPE_ACCESS_CONTROL_PARSE_FAILED alert that
      // lockdown is about to be entered.
      LocalizableMessage lockDownMsg = WARN_ACI_ENTER_LOCKDOWN_MODE.get();
      DirectoryServer.sendAlertNotification(this,
          ALERT_TYPE_ACCESS_CONTROL_PARSE_FAILED, lockDownMsg);
      // Enter lockdown mode.
      DirectoryServer.setLockdownMode(true);

    }
  }
}
