/*
 * 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 2006-2009 Sun Microsystems, Inc.
 *      Portions Copyright 2011-2015 ForgeRock AS
 */
package org.opends.server.api;

import java.net.InetAddress;
import java.nio.channels.ByteChannel;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;

import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.ldap.ByteString;
import org.opends.server.api.plugin.PluginResult;
import org.opends.server.core.AuthenticatedUsers;
import org.opends.server.core.DirectoryServer;
import org.opends.server.core.PersistentSearch;
import org.opends.server.core.PluginConfigManager;
import org.opends.server.core.SearchOperation;
import org.opends.server.types.Attribute;
import org.opends.server.types.AttributeType;
import org.opends.server.types.AuthenticationInfo;
import org.opends.server.types.CancelRequest;
import org.opends.server.types.CancelResult;
import org.opends.server.types.DN;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.DisconnectReason;
import org.opends.server.types.Entry;
import org.opends.server.types.IntermediateResponse;
import org.opends.server.types.Operation;
import org.opends.server.types.Privilege;
import org.opends.server.types.SearchResultEntry;
import org.opends.server.types.SearchResultReference;
import org.opends.server.util.TimeThread;

import static org.opends.messages.CoreMessages.*;
import static org.opends.server.config.ConfigConstants.*;
import static org.opends.server.util.StaticUtils.*;

/**
 * This class defines the set of methods and structures that must be
 * implemented by a Directory Server client connection.
 */
@org.opends.server.types.PublicAPI(
     stability=org.opends.server.types.StabilityLevel.VOLATILE,
     mayInstantiate=true,
     mayExtend=true,
     mayInvoke=true)
public abstract class ClientConnection
{
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();

  /**
   * The set of authentication information for this client connection.
   */
  protected AuthenticationInfo authenticationInfo;

  /**
   * Indicates whether a multistage SASL bind is currently in progress
   * on this client connection.  If so, then no other operations
   * should be allowed until the bind completes.
   */
  protected AtomicBoolean saslBindInProgress;

  /**
   * Indicates if a bind request is currently in progress on this client
   * connection. If so, then no further socket reads will occur until the
   * request completes.
   */
  protected AtomicBoolean bindInProgress;

  /**
   * Indicates if a Start TLS request is currently in progress on this client
   * connection. If so, then no further socket reads will occur until the
   * request completes.
   */
  protected AtomicBoolean startTLSInProgress;

  /**
   *  Indicates whether any necessary finalization work has been done for this
   *  client connection.
   */
  private boolean finalized;

  /** The set of privileges assigned to this client connection. */
  private HashSet<Privilege> privileges = new HashSet<>();

  /** The size limit for use with this client connection. */
  private int sizeLimit;
  /** The time limit for use with this client connection. */
  private int timeLimit;
  /** The lookthrough limit for use with this client connection. */
  private int lookthroughLimit;
  /** The time that this client connection was established. */
  private final long connectTime;
  /** The idle time limit for this client connection. */
  private long idleTimeLimit;

  /**
   * The opaque information used for storing intermediate state information
   * needed across multi-stage SASL binds.
   */
  private Object saslAuthState;

  /** A string representation of the time that this client connection was established. */
  private final String connectTimeString;

  /** A set of persistent searches registered for this client. */
  private final CopyOnWriteArrayList<PersistentSearch> persistentSearches = new CopyOnWriteArrayList<>();

  /** Performs the appropriate initialization generic to all client connections. */
  protected ClientConnection()
  {
    connectTime        = TimeThread.getTime();
    connectTimeString  = TimeThread.getGMTTime();
    authenticationInfo = new AuthenticationInfo();
    saslAuthState      = null;
    saslBindInProgress = new AtomicBoolean(false);
    bindInProgress     = new AtomicBoolean(false);
    startTLSInProgress = new AtomicBoolean(false);
    sizeLimit          = DirectoryServer.getSizeLimit();
    timeLimit          = DirectoryServer.getTimeLimit();
    idleTimeLimit      = DirectoryServer.getIdleTimeLimit();
    lookthroughLimit   = DirectoryServer.getLookthroughLimit();
    finalized          = false;
  }



  /**
   * Performs any internal cleanup that may be necessary when this
   * client connection is disconnected.  In
   * this case, it will be used to ensure that the connection is
   * deregistered with the {@code AuthenticatedUsers} manager, and
   * will then invoke the {@code finalizeClientConnection} method.
   */
 @org.opends.server.types.PublicAPI(
      stability=org.opends.server.types.StabilityLevel.PRIVATE,
      mayInstantiate=false,
      mayExtend=false,
      mayInvoke=true,
      notes="This method should only be invoked by connection " +
             "handlers.")
  protected final void finalizeConnectionInternal()
  {
    if (finalized)
    {
      return;
    }

    finalized = true;

    // Deregister with the set of authenticated users.
    Entry authNEntry = authenticationInfo.getAuthenticationEntry();
    Entry authZEntry = authenticationInfo.getAuthorizationEntry();

    AuthenticatedUsers authenticatedUsers = DirectoryServer.getAuthenticatedUsers();
    if (authNEntry != null)
    {
      if (authZEntry == null || authZEntry.getName().equals(authNEntry.getName()))
      {
        authenticatedUsers.remove(authNEntry.getName(), this);
      }
      else
      {
        authenticatedUsers.remove(authNEntry.getName(), this);
        authenticatedUsers.remove(authZEntry.getName(), this);
      }
    }
    else if (authZEntry != null)
    {
      authenticatedUsers.remove(authZEntry.getName(), this);
    }
  }



  /**
   * Retrieves the time that this connection was established, measured
   * in the number of milliseconds since January 1, 1970 UTC.
   *
   * @return  The time that this connection was established, measured
   *          in the number of milliseconds since January 1, 1970 UTC.
   */
  public final long getConnectTime()
  {
    return connectTime;
  }



  /**
   * Retrieves a string representation of the time that this
   * connection was established.
   *
   * @return  A string representation of the time that this connection
   *          was established.
   */
  public final String getConnectTimeString()
  {
    return connectTimeString;
  }



  /**
   * Retrieves the unique identifier that has been assigned to this
   * connection.
   *
   * @return  The unique identifier that has been assigned to this
   *          connection.
   */
  public abstract long getConnectionID();



  /**
   * Retrieves the connection handler that accepted this client
   * connection.
   *
   * @return  The connection handler that accepted this client
   *          connection.
   */
  public abstract ConnectionHandler<?> getConnectionHandler();



  /**
   * Retrieves the protocol that the client is using to communicate
   * with the Directory Server.
   *
   * @return  The protocol that the client is using to communicate
   *          with the Directory Server.
   */
  public abstract String getProtocol();



  /**
   * Retrieves a string representation of the address of the client.
   *
   * @return  A string representation of the address of the client.
   */
  public abstract String getClientAddress();



  /**
   * Retrieves the port number for this connection on the client
   * system if available.
   *
   * @return The port number for this connection on the client system
   *         or -1 if there is no client port associated with this
   *         connection (e.g. internal client).
   */
  public abstract int getClientPort();



  /**
   * Retrieves the address and port (if available) of the client
   * system, separated by a colon.
   *
   * @return The address and port of the client system, separated by a
   *         colon.
   */
  public final String getClientHostPort()
  {
    int port = getClientPort();
    if (port >= 0)
    {
      return getClientAddress() + ":" + port;
    }
    else
    {
      return getClientAddress();
    }
  }



  /**
   * Retrieves a string representation of the address on the server to
   * which the client connected.
   *
   * @return  A string representation of the address on the server to
   *          which the client connected.
   */
  public abstract String getServerAddress();




  /**
   * Retrieves the port number for this connection on the server
   * system if available.
   *
   * @return The port number for this connection on the server system
   *         or -1 if there is no server port associated with this
   *         connection (e.g. internal client).
   */
  public abstract int getServerPort();



  /**
   * Retrieves the address and port of the server system, separated by
   * a colon.
   *
   * @return The address and port of the server system, separated by a
   *         colon.
   */
  public final String getServerHostPort()
  {
    int port = getServerPort();
    if (port >= 0)
    {
      return getServerAddress() + ":" + port;
    }
    else
    {
      return getServerAddress();
    }
  }



  /**
   * Retrieves the {@code java.net.InetAddress} associated with the
   * remote client system.
   *
   * @return  The {@code java.net.InetAddress} associated with the
   *          remote client system.  It may be {@code null} if the
   *          client is not connected over an IP-based connection.
   */
  public abstract InetAddress getRemoteAddress();



  /**
   * Retrieves the {@code java.net.InetAddress} for the Directory
   * Server system to which the client has established the connection.
   *
   * @return  The {@code java.net.InetAddress} for the Directory
   *          Server system to which the client has established the
   *          connection.  It may be {@code null} if the client is not
   *          connected over an IP-based connection.
   */
  public abstract InetAddress getLocalAddress();

  /**
   * Returns whether the Directory Server believes this connection to be valid
   * and available for communication.
   *
   * @return true if the connection is valid, false otherwise
   */
  public abstract boolean isConnectionValid();

  /**
   * Indicates whether this client connection is currently using a
   * secure mechanism to communicate with the server.  Note that this
   * may change over time based on operations performed by the client
   * or server (e.g., it may go from {@code false} to {@code true} if
   * if the client uses the StartTLS extended operation).
   *
   * @return  {@code true} if the client connection is currently using
   *          a secure mechanism to communicate with the server, or
   *          {@code false} if not.
   */
  public abstract boolean isSecure();


  /**
   * Retrieves a {@code Selector} that may be used to ensure that
   * write  operations complete in a timely manner, or terminate the
   * connection in the event that they fail to do so.  This is an
   * optional method for client connections, and the default
   * implementation returns {@code null} to indicate that the maximum
   * blocked write time limit is not supported for this connection.
   * Subclasses that do wish to support this functionality should
   * return a valid {@code Selector} object.
   *
   * @return  The {@code Selector} that may be used to ensure that
   *          write operations complete in a timely manner, or
   *          {@code null} if this client connection does not support
   *          maximum blocked write time limit functionality.
   */
  public Selector getWriteSelector()
  {
    // There will not be a write selector in the default implementation.
    return null;
  }



  /**
   * Retrieves the maximum length of time in milliseconds that
   * attempts to write data to the client should be allowed to block.
   * A value of zero indicates there should be no limit.
   *
   * @return  The maximum length of time in milliseconds that attempts
   *          to write data to the client should be allowed to block,
   *          or zero if there should be no limit.
   */
  public long getMaxBlockedWriteTimeLimit()
  {
    // By default, we'll return 0, which indicates that there should
    // be no maximum time limit.  Subclasses should override this if
    // they want to support a maximum blocked write time limit.
    return 0L;
  }

  /**
   * Retrieves the total number of operations performed
   * on this connection.
   *
   * @return The total number of operations performed
   * on this connection.
   */
  public abstract long getNumberOfOperations();

  /**
   * Sends a response to the client based on the information in the
   * provided operation.
   *
   * @param  operation  The operation for which to send the response.
   */
  public abstract void sendResponse(Operation operation);



  /**
   * Sends the provided search result entry to the client.
   *
   * @param  searchOperation  The search operation with which the
   *                          entry is associated.
   * @param  searchEntry      The search result entry to be sent to
   *                          the client.
   *
   * @throws  DirectoryException  If a problem occurs while attempting
   *                              to send the entry to the client and
   *                              the search should be terminated.
   */
  public abstract void sendSearchEntry(
                            SearchOperation searchOperation,
                            SearchResultEntry searchEntry)
         throws DirectoryException;



  /**
   * Sends the provided search result reference to the client.
   *
   * @param  searchOperation  The search operation with which the
   *                          reference is associated.
   * @param  searchReference  The search result reference to be sent
   *                          to the client.
   *
   * @return  {@code true} if the client is able to accept referrals,
   *          or {@code false} if the client cannot handle referrals
   *          and no more attempts should be made to send them for the
   *          associated search operation.
   *
   * @throws  DirectoryException  If a problem occurs while attempting
   *                              to send the reference to the client
   *                              and the search should be terminated.
   */
  public abstract boolean sendSearchReference(
                               SearchOperation searchOperation,
                               SearchResultReference searchReference)
         throws DirectoryException;



  /**
   * Invokes the intermediate response plugins on the provided
   * response message and sends it to the client.
   *
   * @param  intermediateResponse  The intermediate response message
   *                               to be sent.
   *
   * @return  {@code true} if processing on the associated operation
   *          should continue, or {@code false} if not.
   */
  public final boolean sendIntermediateResponse(
                            IntermediateResponse intermediateResponse)
  {
    // Invoke the intermediate response plugins for the response message.
    PluginConfigManager pluginConfigManager =
         DirectoryServer.getPluginConfigManager();
    PluginResult.IntermediateResponse pluginResult =
         pluginConfigManager.invokeIntermediateResponsePlugins(
                                  intermediateResponse);

    boolean continueProcessing = true;
    if (pluginResult.sendResponse())
    {
      continueProcessing =
           sendIntermediateResponseMessage(intermediateResponse);
    }

    return continueProcessing && pluginResult.continueProcessing();
  }




  /**
   * Sends the provided intermediate response message to the client.
   *
   * @param  intermediateResponse  The intermediate response message
   *                               to be sent.
   *
   * @return  {@code true} if processing on the associated operation
   *          should continue, or {@code false} if not.
   */
  protected abstract boolean
       sendIntermediateResponseMessage(
            IntermediateResponse intermediateResponse);



  /**
   * Closes the connection to the client, optionally sending it a
   * message indicating the reason for the closure.  Note that the
   * ability to send a notice of disconnection may not be available
   * for all protocols or under all circumstances.  Also note that
   * when attempting to disconnect a client connection as a part of
   * operation processing (e.g., within a plugin or other extension),
   * the {@code disconnectClient} method within that operation should
   * be called rather than invoking this method directly.
   * <BR><BR>
   * All subclasses must invoke the {@code finalizeConnectionInternal}
   * method during the course of processing this method.
   *
   * @param  disconnectReason  The disconnect reason that provides the
   *                           generic cause for the disconnect.
   * @param  sendNotification  Indicates whether to try to provide
   *                           notification to the client that the
   *                           connection will be closed.
   * @param  message           The message to send to the client.  It
   *                           may be {@code null} if no notification
   *                           is to be sent.
   */
  public abstract void disconnect(DisconnectReason disconnectReason,
                                  boolean sendNotification,
                                  LocalizableMessage message);



  /**
   * Indicates whether the user associated with this client connection
   * must change their password before they will be allowed to do
   * anything else.
   *
   * @return  {@code true} if the user associated with this client
   *          connection must change their password before they will
   *          be allowed to do anything else, or {@code false} if not.
   */
  public final boolean mustChangePassword()
  {
    return authenticationInfo != null
        && authenticationInfo.mustChangePassword();
  }



  /**
   * Specifies whether the user associated with this client connection
   * must change their password before they will be allowed to do
   * anything else.
   *
   * @param  mustChangePassword  Specifies whether the user associated
   *                             with this client connection must
   *                             change their password before they
   *                             will be allowed to do anything else.
   */
  public final void setMustChangePassword(boolean mustChangePassword)
  {
    if (authenticationInfo == null)
    {
      setAuthenticationInfo(new AuthenticationInfo());
    }

    authenticationInfo.setMustChangePassword(mustChangePassword);
  }



  /**
   * Retrieves the set of operations in progress for this client
   * connection.  This list must not be altered by any caller.
   *
   * @return  The set of operations in progress for this client
   *          connection.
   */
  public abstract Collection<Operation> getOperationsInProgress();



  /**
   * Retrieves the operation in progress with the specified message ID.
   *
   * @param  messageID  The message ID of the operation to retrieve.
   * @return  The operation in progress with the specified message ID,
   *          or {@code null} if no such operation could be found.
   */
  public abstract Operation getOperationInProgress(int messageID);



  /**
   * Removes the provided operation from the set of operations in
   * progress for this client connection.  Note that this does not
   * make any attempt to cancel any processing that may already be in
   * progress for the operation.
   *
   * @param  messageID  The message ID of the operation to remove from
   *                    the set of operations in progress.
   * @return  {@code true} if the operation was found and removed from
   *          the set of operations in progress, or {@code false} if not.
   */
  public abstract boolean removeOperationInProgress(int messageID);



  /**
   * Retrieves the set of persistent searches registered for this client.
   *
   * @return  The set of persistent searches registered for this client.
   */
  public final List<PersistentSearch> getPersistentSearches()
  {
    return persistentSearches;
  }



  /**
   * Registers the provided persistent search for this client.
   * Note that this should only be called by
   * {@code DirectoryServer.registerPersistentSearch} and not through any other means.
   *
   * @param  persistentSearch  The persistent search to register for this client.
   */
 @org.opends.server.types.PublicAPI(
      stability=org.opends.server.types.StabilityLevel.PRIVATE,
      mayInstantiate=false,
      mayExtend=false,
      mayInvoke=false)
  public final void registerPersistentSearch(PersistentSearch
                                                  persistentSearch)
  {
    persistentSearches.add(persistentSearch);
  }



  /**
   * Deregisters the provided persistent search for this client.  Note
   * that this should only be called by
   * {@code DirectoryServer.deregisterPersistentSearch} and not
   * through any other means.
   *
   * @param  persistentSearch  The persistent search to deregister for
   *                           this client.
   */
 @org.opends.server.types.PublicAPI(
      stability=org.opends.server.types.StabilityLevel.PRIVATE,
      mayInstantiate=false,
      mayExtend=false,
      mayInvoke=false)
  public final void deregisterPersistentSearch(PersistentSearch
                                                    persistentSearch)
  {
    persistentSearches.remove(persistentSearch);
  }



  /**
   * Attempts to cancel the specified operation.
   *
   * @param  messageID      The message ID of the operation to cancel.
   * @param  cancelRequest  An object providing additional information
   *                        about how the cancel should be processed.
   *
   * @return  A cancel result that either indicates that the cancel
   *          was successful or provides a reason that it was not.
   */
  public abstract CancelResult cancelOperation(int messageID,
                                    CancelRequest cancelRequest);



  /**
   * Attempts to cancel all operations in progress on this connection.
   *
   * @param  cancelRequest  An object providing additional information
   *                        about how the cancel should be processed.
   */
  public abstract void cancelAllOperations(
                            CancelRequest cancelRequest);



  /**
   * Attempts to cancel all operations in progress on this connection
   * except the operation with the specified message ID.
   *
   * @param  cancelRequest  An object providing additional information
   *                        about how the cancel should be processed.
   * @param  messageID      The message ID of the operation that
   *                        should not be canceled.
   */
  public abstract void cancelAllOperationsExcept(
                            CancelRequest cancelRequest,
                            int messageID);



  /**
   * Retrieves information about the authentication that has been
   * performed for this connection.
   *
   * @return  Information about the user that is currently
   *          authenticated on this connection.
   */
  public AuthenticationInfo getAuthenticationInfo()
  {
    return authenticationInfo;
  }



  /**
   * Specifies information about the authentication that has been
   * performed for this connection.
   *
   * @param  authenticationInfo  Information about the authentication
   *                             that has been performed for this
   *                             connection.  It should not be
   *                             {@code null}.
   */
  public void setAuthenticationInfo(AuthenticationInfo
                                         authenticationInfo)
  {
    AuthenticatedUsers authenticatedUsers = DirectoryServer.getAuthenticatedUsers();
    if (this.authenticationInfo != null)
    {
      Entry authNEntry = this.authenticationInfo.getAuthenticationEntry();
      Entry authZEntry = this.authenticationInfo.getAuthorizationEntry();

      if (authNEntry != null)
      {
        if (authZEntry == null ||
            authZEntry.getName().equals(authNEntry.getName()))
        {
          authenticatedUsers.remove(authNEntry.getName(), this);
        }
        else
        {
          authenticatedUsers.remove(authNEntry.getName(), this);
          authenticatedUsers.remove(authZEntry.getName(), this);
        }
      }
      else if (authZEntry != null)
      {
        authenticatedUsers.remove(authZEntry.getName(), this);
      }
    }

    if (authenticationInfo == null)
    {
      this.authenticationInfo = new AuthenticationInfo();
      updatePrivileges(null, false);
    }
    else
    {
      this.authenticationInfo = authenticationInfo;

      Entry authNEntry = authenticationInfo.getAuthenticationEntry();
      Entry authZEntry = authenticationInfo.getAuthorizationEntry();

      if (authNEntry != null)
      {
        if (authZEntry == null || authZEntry.getName().equals(authNEntry.getName()))
        {
          authenticatedUsers.put(authNEntry.getName(), this);
        }
        else
        {
          authenticatedUsers.put(authNEntry.getName(), this);
          authenticatedUsers.put(authZEntry.getName(), this);
        }
      }
      else
      {
        if (authZEntry != null)
        {
          authenticatedUsers.put(authZEntry.getName(), this);
        }
      }

      updatePrivileges(authZEntry, authenticationInfo.isRoot());
    }
  }



  /**
   * Updates the cached entry associated with either the
   * authentication and/or authorization identity with the provided
   * version.
   *
   * @param  oldEntry  The user entry currently serving as the
   *                   authentication and/or authorization identity.
   * @param  newEntry  The updated entry that should replace the
   *                   existing entry.  It may optionally have a
   *                   different DN than the old entry.
   */
  public final void updateAuthenticationInfo(Entry oldEntry,
                                             Entry newEntry)
  {
    Entry authNEntry = authenticationInfo.getAuthenticationEntry();
    Entry authZEntry = authenticationInfo.getAuthorizationEntry();

    if (authNEntry != null && authNEntry.getName().equals(oldEntry.getName()))
    {
      if (authZEntry == null || !authZEntry.getName().equals(authNEntry.getName()))
      {
        setAuthenticationInfo(
             authenticationInfo.duplicate(newEntry, authZEntry));
        updatePrivileges(newEntry, authenticationInfo.isRoot());
      }
      else
      {
        setAuthenticationInfo(
             authenticationInfo.duplicate(newEntry, newEntry));
        updatePrivileges(newEntry, authenticationInfo.isRoot());
      }
    }
    else if (authZEntry != null && authZEntry.getName().equals(oldEntry.getName()))
    {
      setAuthenticationInfo(
           authenticationInfo.duplicate(authNEntry, newEntry));
    }
  }



  /**
   * Sets properties in this client connection to indicate that the
   * client is unauthenticated.  This includes setting the
   * authentication info structure to an empty default, as well as
   * setting the size and time limit values to their defaults.
   */
  public void setUnauthenticated()
  {
    setAuthenticationInfo(new AuthenticationInfo());
  }


  /**
   * Indicate whether the specified authorization entry parameter
   * has the specified privilege. The method can be used to perform
   * a "what-if" scenario.
   *
 * @param authorizationEntry The authentication entry to use.
 * @param privilege The privilege to check for.
   *
   * @return  {@code true} if the authentication entry has the
   *          specified privilege, or {@code false} if not.
   */
  public static boolean hasPrivilege(Entry authorizationEntry,
                                   Privilege privilege) {
      boolean isRoot =
          DirectoryServer.isRootDN(authorizationEntry.getName());
      return getPrivileges(authorizationEntry,
              isRoot).contains(privilege) ||
              DirectoryServer.isDisabled(privilege);
  }


  /**
   * Indicates whether the authenticated client has the specified
   * privilege.
   *
   * @param  privilege  The privilege for which to make the
   *                    determination.
   * @param  operation  The operation being processed which needs to
   *                    make the privilege determination, or
   *                    {@code null} if there is no associated
   *                    operation.
   *
   * @return  {@code true} if the authenticated client has the
   *          specified privilege, or {@code false} if not.
   */
  public boolean hasPrivilege(Privilege privilege,
                              Operation operation)
  {
    if (privilege == Privilege.PROXIED_AUTH)
    {
      // This determination should always be made against the
      // authentication identity rather than the authorization
      // identity.
      Entry authEntry = authenticationInfo.getAuthenticationEntry();
      boolean isRoot  = authenticationInfo.isRoot();
      return getPrivileges(authEntry, isRoot).contains(Privilege.PROXIED_AUTH) ||
             DirectoryServer.isDisabled(Privilege.PROXIED_AUTH);
    }

    boolean result;
    if (operation == null)
    {
      result = privileges.contains(privilege);
      logger.trace(INFO_CLIENTCONNECTION_AUDIT_HASPRIVILEGE,
          getConnectionID(), -1L, authenticationInfo.getAuthenticationDN(),
          privilege.getName(), result);
    }
    else
    {
      if (operation.getAuthorizationDN().equals(
               authenticationInfo.getAuthorizationDN()) ||
          (operation.getAuthorizationDN().equals(DN.NULL_DN) &&
           !authenticationInfo.isAuthenticated())) {
        result = privileges.contains(privilege) ||
                 DirectoryServer.isDisabled(privilege);
        logger.trace(INFO_CLIENTCONNECTION_AUDIT_HASPRIVILEGE,
            getConnectionID(), operation.getOperationID(),
            authenticationInfo.getAuthenticationDN(),
            privilege.getName(), result);
      }
      else
      {
        Entry authorizationEntry = operation.getAuthorizationEntry();
        if (authorizationEntry == null)
        {
          result = false;
        }
        else
        {
          boolean isRoot =
               DirectoryServer.isRootDN(authorizationEntry.getName());
          result = getPrivileges(authorizationEntry,
                                 isRoot).contains(privilege) ||
                   DirectoryServer.isDisabled(privilege);
        }
      }
    }

    return result;
  }



  /**
   * Indicates whether the authenticate client has all of the
   * specified privileges.
   *
   * @param  privileges  The array of privileges for which to make the
   *                     determination.
   * @param  operation   The operation being processed which needs to
   *                     make the privilege determination, or
   *                     {@code null} if there is no associated
   *                     operation.
   *
   * @return  {@code true} if the authenticated client has all of the
   *          specified privileges, or {@code false} if not.
   */
  public boolean hasAllPrivileges(Privilege[] privileges, Operation operation)
  {
    final boolean result = hasAllPrivileges0(this.privileges, privileges);
    if (logger.isTraceEnabled())
    {
      long operationID = operation != null ? operation.getOperationID() : -1;
      final DN authDN = authenticationInfo.getAuthenticationDN();
      StringBuilder buffer = toStringBuilder(privileges);
      logger.trace(INFO_CLIENTCONNECTION_AUDIT_HASPRIVILEGES, getConnectionID(), operationID, authDN, buffer, result);
    }
    return result;
  }

  private boolean hasAllPrivileges0(Set<Privilege> privSet, Privilege[] privileges)
  {
    for (Privilege p : privileges)
    {
      if (!privSet.contains(p))
      {
        return false;
      }
    }
    return true;
  }

  private StringBuilder toStringBuilder(Privilege[] privileges)
  {
    StringBuilder buffer = new StringBuilder();
    buffer.append("{");
    for (int i = 0; i < privileges.length; i++)
    {
      Privilege privilege = privileges[i];
      if (i > 0)
      {
        buffer.append(",");
      }
      buffer.append(privilege.getName());
    }
    buffer.append(" }");
    return buffer;
  }

  /**
   * Retrieves the set of privileges encoded in the provided entry.
   *
   * @param entry
   *          The entry to use to obtain the privilege information.
   * @param isRoot
   *          Indicates whether the set of root privileges should be automatically included in the
   *          privilege set.
   * @return A set of the privileges that should be assigned.
   */
  private static HashSet<Privilege> getPrivileges(Entry entry,
                                           boolean isRoot)
  {
    if (entry == null)
    {
      return new HashSet<>(0);
    }

    HashSet<Privilege> newPrivileges = new HashSet<>();
    HashSet<Privilege> removePrivileges = new HashSet<>();

    if (isRoot)
    {
      newPrivileges.addAll(DirectoryServer.getRootPrivileges());
    }

    AttributeType privType = DirectoryServer.getAttributeTypeOrNull(OP_ATTR_PRIVILEGE_NAME);
    List<Attribute> attrList = entry.getAttribute(privType);
    if (attrList != null)
    {
      for (Attribute a : attrList)
      {
        for (ByteString v : a)
        {
          String privName = toLowerCase(v.toString());

          // If the name of the privilege is prefixed with a minus
          // sign, then we will take away that privilege from the
          // user.  We'll handle that at the end so that we can make
          // sure it's not added back later.
          if (privName.startsWith("-"))
          {
            privName = privName.substring(1);
            Privilege p = Privilege.privilegeForName(privName);
            if (p == null)
            {
              // FIXME -- Generate an administrative alert.

              // We don't know what privilege to remove, so we'll
              // remove all of them.
              newPrivileges.clear();
              return newPrivileges;
            }
            else
            {
              removePrivileges.add(p);
            }
          }
          else
          {
            Privilege p = Privilege.privilegeForName(privName);
            if (p == null)
            {
              // FIXME -- Generate an administrative alert.
            }
            else
            {
              newPrivileges.add(p);
            }
          }
        }
      }
    }

    newPrivileges.removeAll(removePrivileges);

    return newPrivileges;
  }



  /**
   * Updates the privileges associated with this client connection
   * object based on the provided entry for the authentication
   * identity.
   *
   * @param  entry   The entry for the authentication identity
   *                 associated with this client connection.
   * @param  isRoot  Indicates whether the associated user is a root
   *                 user and should automatically inherit the root
   *                 privilege set.
   */
  protected void updatePrivileges(Entry entry, boolean isRoot)
  {
    privileges = getPrivileges(entry, isRoot);
  }



  /**
   * Retrieves an opaque set of information that may be used for
   * processing multi-stage SASL binds.
   *
   * @return  An opaque set of information that may be used for
   *          processing multi-stage SASL binds.
   */
  public final Object getSASLAuthStateInfo()
  {
    return saslAuthState;
  }



  /**
   * Specifies an opaque set of information that may be used for
   * processing multi-stage SASL binds.
   *
   * @param  saslAuthState  An opaque set of information that may be
   *                        used for processing multi-stage SASL
   *                        binds.
   */
  public final void setSASLAuthStateInfo(Object saslAuthState)
  {
    this.saslAuthState = saslAuthState;
  }


  /**
   * Return the lowest level channel associated with a connection.
   * This is normally the channel associated with the socket
   * channel.
   *
   * @return The lowest level channel associated with a connection.
   */
  public ByteChannel getChannel() {
    // By default, return null, which indicates that there should
    // be no channel.  Subclasses should override this if
    // they want to support a channel.
    return null;
  }



  /**
   * Return the Socket channel associated with a connection.
   *
   * @return The Socket channel associated with a connection.
   */
  public SocketChannel getSocketChannel() {
    // By default, return null, which indicates that there should
    // be no socket channel.  Subclasses should override this if
    // they want to support a socket channel.
    return null;
  }



  /**
   * Retrieves the size limit that will be enforced for searches
   * performed using this client connection.
   *
   * @return  The size limit that will be enforced for searches
   *          performed using this client connection.
   */
  public final int getSizeLimit()
  {
    return sizeLimit;
  }



  /**
   * Specifies the size limit that will be enforced for searches
   * performed using this client connection.
   *
   * @param  sizeLimit  The size limit that will be enforced for
   *                    searches performed using this client
   *                    connection.
   */
  public void setSizeLimit(int sizeLimit)
  {
    this.sizeLimit = sizeLimit;
  }



  /**
   * Retrieves the maximum length of time in milliseconds that this
   * client connection will be allowed to remain idle before it should
   * be disconnected.
   *
   * @return  The maximum length of time in milliseconds that this
   *          client connection will be allowed to remain idle before
   *          it should be disconnected.
   */
  public final long getIdleTimeLimit()
  {
    return idleTimeLimit;
  }



  /**
   * Specifies the maximum length of time in milliseconds that this
   * client connection will be allowed to remain idle before it should
   * be disconnected.
   *
   * @param  idleTimeLimit  The maximum length of time in milliseconds
   *                        that this client connection will be
   *                        allowed to remain idle before it should be
   *                        disconnected.
   */
  public void setIdleTimeLimit(long idleTimeLimit)
  {
    this.idleTimeLimit = idleTimeLimit;
  }



  /**
   * Retrieves the default maximum number of entries that should
   * checked for matches during a search.
   *
   * @return  The default maximum number of entries that should
   *          checked for matches during a search.
   */
  public final int getLookthroughLimit()
  {
    return lookthroughLimit;
  }



  /**
   * Specifies the default maximum number of entries that should
   * be checked for matches during a search.
   *
   * @param  lookthroughLimit  The default maximum number of
   *                           entries that should be check for
   *                           matches during a search.
   */
  public void setLookthroughLimit(int lookthroughLimit)
  {
    this.lookthroughLimit = lookthroughLimit;
  }



  /**
   * Retrieves the time limit that will be enforced for searches
   * performed using this client connection.
   *
   * @return  The time limit that will be enforced for searches
   *          performed using this client connection.
   */
  public final int getTimeLimit()
  {
    return timeLimit;
  }



  /**
   * Specifies the time limit that will be enforced for searches
   * performed using this client connection.
   *
   * @param  timeLimit  The time limit that will be enforced for
   *                    searches performed using this client
   *                    connection.
   */
  public void setTimeLimit(int timeLimit)
  {
    this.timeLimit = timeLimit;
  }



  /**
   * Retrieves a one-line summary of this client connection in a form
   * that is suitable for including in the monitor entry for the
   * associated connection handler.  It should be in a format that is
   * both humand readable and machine parseable (e.g., a
   * space-delimited name-value list, with quotes around the values).
   *
   * @return  A one-line summary of this client connection in a form
   *          that is suitable for including in the monitor entry for
   *          the associated connection handler.
   */
  public abstract String getMonitorSummary();



  /**
   * Indicates whether the user associated with this client connection
   * should be considered a member of the specified group, optionally
   * evaluated within the context of the provided operation.  If an
   * operation is given, then the determination should be made based
   * on the authorization identity for that operation.  If the
   * operation is {@code null}, then the determination should be made
   * based on the authorization identity for this client connection.
   * Note that this is a point-in-time determination and the caller
   * must not cache the result.
   *
   * @param  group      The group for which to make the determination.
   * @param  operation  The operation to use to obtain the
   *                    authorization identity for which to make the
   *                    determination, or {@code null} if the
   *                    authorization identity should be obtained from
   *                    this client connection.
   *
   * @return  {@code true} if the target user is currently a member of
   *          the specified group, or {@code false} if not.
   *
   * @throws  DirectoryException  If a problem occurs while attempting
   *                             to make the determination.
   */
  public boolean isMemberOf(Group<?> group, Operation operation)
         throws DirectoryException
  {
    if (operation == null)
    {
      return group.isMember(authenticationInfo.getAuthorizationDN());
    }
    else
    {
      return group.isMember(operation.getAuthorizationDN());
    }
  }



  /**
   * Retrieves the set of groups in which the user associated with
   * this client connection may be considered to be a member.  If an
   * operation is provided, then the determination should be made
   * based on the authorization identity for that operation.  If the
   * operation is {@code null}, then it should be made based on the
   * authorization identity for this client connection.  Note that
   * this is a point-in-time determination and the caller must not
   * cache the result.
   *
   * @param  operation  The operation to use to obtain the
   *                    authorization identity for which to retrieve
   *                    the associated groups, or {@code null} if the
   *                    authorization identity should be obtained from
   *                    this client connection.
   *
   * @return  The set of groups in which the target user is currently
   *          a member.
   *
   * @throws  DirectoryException  If a problem occurs while attempting
   *                              to make the determination.
   */
  public Set<Group<?>> getGroups(Operation operation)
         throws DirectoryException
  {
    // FIXME -- This probably isn't the most efficient implementation.
    DN authzDN;
    if (operation == null)
    {
      if (authenticationInfo == null || !authenticationInfo.isAuthenticated())
      {
        authzDN = null;
      }
      else
      {
        authzDN = authenticationInfo.getAuthorizationDN();
      }
    }
    else
    {
      authzDN = operation.getAuthorizationDN();
    }

    if (authzDN == null || authzDN.isRootDN())
    {
      return Collections.<Group<?>>emptySet();
    }

    Entry userEntry = DirectoryServer.getEntry(authzDN);
    if (userEntry == null)
    {
      return Collections.<Group<?>>emptySet();
    }

    HashSet<Group<?>> groupSet = new HashSet<>();
    for (Group<?> g : DirectoryServer.getGroupManager().getGroupInstances())
    {
      if (g.isMember(userEntry))
      {
        groupSet.add(g);
      }
    }
    return groupSet;
  }



  /**
   * Retrieves the DN of the key manager provider that should be used
   * for operations requiring access to a key manager.  The default
   * implementation returns {@code null} to indicate that no key
   * manager provider is available, but subclasses should override
   * this method to return a valid DN if they perform operations which
   * may need access to a key manager.
   *
   * @return  The DN of the key manager provider that should be used
   *          for operations requiring access to a key manager, or
   *          {@code null} if there is no key manager provider
   *          configured for this client connection.
   */
  public DN getKeyManagerProviderDN()
  {
    // In the default implementation, we'll return null.
    return null;
  }



  /**
   * Retrieves the DN of the trust manager provider that should be
   * used for operations requiring access to a trust manager.  The
   * default implementation returns {@code null} to indicate that no
   * trust manager provider is available, but subclasses should
   * override this method to return a valid DN if they perform
   * operations which may need access to a trust manager.
   *
   * @return  The DN of the trust manager provider that should be used
   *          for operations requiring access to a trust manager, or
   *          {@code null} if there is no trust manager provider
   *          configured for this client connection.
   */
  public DN getTrustManagerProviderDN()
  {
    // In the default implementation, we'll return null.
    return null;
  }



  /**
   * Retrieves the alias of the server certificate that should be used
   * for operations requiring a server certificate.  The default
   * implementation returns {@code null} to indicate that any alias is
   * acceptable.
   *
   * @return  The alias of the server certificate that should be used
   *          for operations requiring a server certificate, or
   *          {@code null} if any alias is acceptable.
   */
  public String getCertificateAlias()
  {
    // In the default implementation, we'll return null.
    return null;
  }



  /**
   * Retrieves a string representation of this client connection.
   *
   * @return  A string representation of this client connection.
   */
  @Override
  public final String toString()
  {
    StringBuilder buffer = new StringBuilder();
    toString(buffer);
    return buffer.toString();
  }



  /**
   * Appends a string representation of this client connection to the
   * provided buffer.
   *
   * @param  buffer  The buffer to which the information should be
   *                 appended.
   */
  public abstract void toString(StringBuilder buffer);

  /**
   * Retrieves the length of time in milliseconds that this client
   * connection has been idle.
   * <BR><BR>
   * Note that the default implementation will always return zero.
   * Subclasses associated with connection handlers should override
   * this method if they wish to provided idle time limit
   * functionality.
   *
   * @return  The length of time in milliseconds that this client
   *          connection has been idle.
   */
  public long getIdleTime()
  {
    return 0L;
  }

  /**
   * Return the Security Strength Factor of a client connection.
   *
   * @return An integer representing the SSF value of a connection.
   */
  public abstract int getSSF();

  /**
   * Indicates a bind or start TLS request processing is finished
   * and the client connection may start processing data read from
   * the socket again. This must be called after processing each
   * bind request in a multistage SASL bind.
   */
  public void finishBind()
  {
    bindInProgress.set(false);
  }

  /**
   * Indicates a bind or start TLS request processing is finished
   * and the client connection may start processing data read from
   * the socket again. This must be called after processing each
   * bind request in a multistage SASL bind.
   */
  public void finishStartTLS()
  {
    startTLSInProgress.set(false);
  }

  /**
   * Indicates a multistage SASL bind operation is finished and the
   * client connection may accept additional LDAP messages.
   */
  public void finishSaslBind()
  {
    saslBindInProgress.set(false);
  }

  /**
   * Returns whether this connection is used for inner work not directly
   * requested by an external client.
   *
   * @return {@code true} if this is an inner connection, {@code false}
   *         otherwise
   */
  public boolean isInnerConnection()
  {
    return getConnectionID() < 0;
  }
}
