mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

neil_a_wilson
29.58.2007 b4e7c2adbcf17bb3017ffc4aad6d395187bc4e59
Add support for a new disconnect client task that can be used to allow an
administrator to terminate a client connection if the need arises. The
requester must have the disconnect-client privilege. The task entry should
contain the ds-task-disconnect object class, which requires the
ds-task-disconnect-connection-id attribute type and optionally allows the
ds-task-disconnect-notify-client and ds-task-disconnect-message attribute
types.

Also, add support for a "Get Connection ID" extended operation, which allows a
client to determine the connection ID associated with its connection in the
server.

OpenDS Issue Numbers: 429, 478, 2025
3 files added
4 files modified
793 ■■■■■ changed files
opends/resource/config/config.ldif 7 ●●●●● patch | view | raw | blame | history
opends/resource/schema/02-config.ldif 15 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/extensions/GetConnectionIDExtendedOperation.java 139 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/messages/TaskMessages.java 90 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/tasks/DisconnectClientTask.java 236 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/util/ServerConstants.java 35 ●●●●● patch | view | raw | blame | history
opends/tests/unit-tests-testng/src/server/org/opends/server/tasks/DisconnectClientTaskTestCase.java 271 ●●●●● patch | view | raw | blame | history
opends/resource/config/config.ldif
@@ -405,6 +405,13 @@
ds-cfg-extended-operation-handler-class: org.opends.server.extensions.CancelExtendedOperation
ds-cfg-extended-operation-handler-enabled: true
dn: cn=Get Connection ID,cn=Extended Operations,cn=config
objectClass: top
objectClass: ds-cfg-extended-operation-handler
cn: Cancel
ds-cfg-extended-operation-handler-class: org.opends.server.extensions.GetConnectionIDExtendedOperation
ds-cfg-extended-operation-handler-enabled: true
dn: cn=Password Modify,cn=Extended Operations,cn=config
objectClass: top
objectClass: ds-cfg-extended-operation-handler
opends/resource/schema/02-config.ldif
@@ -1520,6 +1520,16 @@
  NAME 'ds-task-import-clear-backend'
  SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE
  X-ORIGIN 'OpenDS Directory Server' )
attributeTypes: ( 1.3.6.1.4.1.26027.1.1.453
  NAME 'ds-task-disconnect-connection-id'
  SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE
  X-ORIGIN 'OpenDS Directory Server' )
attributeTypes: ( 1.3.6.1.4.1.26027.1.1.454 NAME 'ds-task-disconnect-message'
  SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE
  X-ORIGIN 'OpenDS Directory Server' )
attributeTypes: ( 1.3.6.1.4.1.26027.1.1.455
  NAME 'ds-task-disconnect-notify-client' SYNTAX 1.3.6.1.4.1.1466.115.121.1.7
  SINGLE-VALUE X-ORIGIN 'OpenDS Directory Server' )
objectClasses: ( 1.3.6.1.4.1.26027.1.2.1
  NAME 'ds-cfg-access-control-handler' SUP top STRUCTURAL
  MUST ( cn $ ds-cfg-acl-handler-class $ ds-cfg-acl-handler-enabled )
@@ -2138,4 +2148,9 @@
  MUST ( ds-cfg-sender-address $ ds-cfg-recipient-address $
  ds-cfg-message-subject $ ds-cfg-message-body )
  X-ORIGIN 'OpenDS Directory Server' )
objectClasses: ( 1.3.6.1.4.1.26027.1.2.119
  NAME 'ds-task-disconnect' SUP ds-task STRUCTURAL
  MUST ds-task-disconnect-connection-id
  MAY ( ds-task-disconnect-message $ ds-task-disconnect-notify-client )
  X-ORIGIN 'OpenDS Directory Server' )
opends/src/server/org/opends/server/extensions/GetConnectionIDExtendedOperation.java
New file
@@ -0,0 +1,139 @@
/*
 * 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
 *
 *
 *      Portions Copyright 2007 Sun Microsystems, Inc.
 */
package org.opends.server.extensions;
import org.opends.server.admin.std.server.ExtendedOperationHandlerCfg;
import org.opends.server.api.ExtendedOperationHandler;
import org.opends.server.config.ConfigException;
import org.opends.server.core.DirectoryServer;
import org.opends.server.core.ExtendedOperation;
import org.opends.server.protocols.asn1.ASN1Exception;
import org.opends.server.protocols.asn1.ASN1Long;
import org.opends.server.protocols.asn1.ASN1OctetString;
import org.opends.server.types.InitializationException;
import org.opends.server.types.ResultCode;
import static org.opends.server.messages.ExtensionsMessages.*;
import static org.opends.server.messages.MessageHandler.*;
import static org.opends.server.util.ServerConstants.*;
/**
 * This class implements the "Get Connection ID" extended operation that can be
 * used to get the connection ID of the associated client connection.
 */
public class GetConnectionIDExtendedOperation
       extends ExtendedOperationHandler<ExtendedOperationHandlerCfg>
{
  /**
   * Create an instance of this "Get Connection ID" extended operation.  All
   * initialization should be performed in the
   * {@code initializeExtendedOperationHandler} method.
   */
  public GetConnectionIDExtendedOperation()
  {
    super();
  }
  /**
   * {@inheritDoc}
   */
  public void initializeExtendedOperationHandler(
       ExtendedOperationHandlerCfg config)
       throws ConfigException, InitializationException
  {
    // No special configuration is required.
    DirectoryServer.registerSupportedExtension(OID_GET_CONNECTION_ID_EXTOP,
                                               this);
    registerControlsAndFeatures();
  }
  /**
   * {@inheritDoc}
   */
  public void finalizeExtendedOperationHandler()
  {
    DirectoryServer.deregisterSupportedExtension(OID_WHO_AM_I_REQUEST);
    deregisterControlsAndFeatures();
  }
  /**
   * {@inheritDoc}
   */
  public void processExtendedOperation(ExtendedOperation operation)
  {
    operation.setResponseOID(OID_GET_CONNECTION_ID_EXTOP);
    operation.setResponseValue(
         encodeResponseValue(operation.getConnectionID()));
    operation.setResultCode(ResultCode.SUCCESS);
  }
  /**
   * Encodes the provided connection ID in an octet string suitable for use as
   * the value for this extended operation.
   *
   * @param  connectionID  The connection ID to be encoded.
   *
   * @return  The ASN.1 octet string containing the encoded connection ID.
   */
  public static ASN1OctetString encodeResponseValue(long connectionID)
  {
    return new ASN1OctetString(new ASN1Long(connectionID).encode());
  }
  /**
   * Decodes the provided ASN.1 octet string to extract the connection ID.
   *
   * @param  responseValue  The response value to be decoded.
   *
   * @return  The connection ID decoded from the provided response value.
   *
   * @throws  ASN1Exception  If an error occurs while trying to decode the
   *                         response value.
   */
  public static long decodeResponseValue(ASN1OctetString responseValue)
         throws ASN1Exception
  {
    return ASN1Long.decodeAsLong(responseValue.value()).longValue();
  }
}
opends/src/server/org/opends/server/messages/TaskMessages.java
@@ -274,6 +274,76 @@
  /**
   * The message ID for the message that will be used if the client does not
   * have the DISCONNECT_CLIENT privilege.  It does not take any arguments.
   */
  public static final int  MSGID_TASK_DISCONNECT_NO_PRIVILEGE =
       CATEGORY_MASK_TASK | SEVERITY_MASK_SEVERE_ERROR | 25;
  /**
   * The message ID for the message that will be used if the provided connection
   * ID cannot be decoded.  This takes a single argument, which is the invalid
   * value.
   */
  public static final int  MSGID_TASK_DISCONNECT_INVALID_CONN_ID =
       CATEGORY_MASK_TASK | SEVERITY_MASK_SEVERE_ERROR | 26;
  /**
   * The message ID for the message that will be used if the task entry does
   * not specify a target connection ID.  This takes a single argument, which is
   * the name of the attribute type used to specify the target connection ID.
   */
  public static final int  MSGID_TASK_DISCONNECT_NO_CONN_ID =
       CATEGORY_MASK_TASK | SEVERITY_MASK_SEVERE_ERROR | 27;
  /**
   * The message ID for the message that will be used if the notifyClient
   * attribute value cannot be decoded.  This takes a single argument, which is
   * the invalid value.
   */
  public static final int  MSGID_TASK_DISCONNECT_INVALID_NOTIFY_CLIENT =
       CATEGORY_MASK_TASK | SEVERITY_MASK_SEVERE_ERROR | 28;
  /**
   * The message ID for the message that will be used as a generic message that
   * may be sent to the client if no other value is given.  It does not take
   * any arguments.
   */
  public static final int  MSGID_TASK_DISCONNECT_GENERIC_MESSAGE =
       CATEGORY_MASK_TASK | SEVERITY_MASK_INFORMATIONAL | 29;
  /**
   * The message ID for the message that will be used if no client connection
   * can be found with the specified connection ID.  It takes a single argument,
   * which is the target connection ID.
   */
  public static final int  MSGID_TASK_DISCONNECT_NO_SUCH_CONNECTION =
       CATEGORY_MASK_TASK | SEVERITY_MASK_SEVERE_ERROR | 30;
  /**
   * The message ID for the message that will be used as the message ID for the
   * disconnectClient method.  The associated message will be defined, but it
   * will not actually be used, since only the message ID is needed.  It does
   * not take any arguments.
   */
  public static final int  MSGID_TASK_DISCONNECT_MESSAGE =
       CATEGORY_MASK_TASK | SEVERITY_MASK_INFORMATIONAL | 31;
  /**
   * Associates a set of generic messages with the message IDs defined in this
   * class.
   */
@@ -362,6 +432,26 @@
    registerMessage(MSGID_TASK_LEAVELOCKDOWN_NOT_LOOPBACK,
                    "Only root users connected from a loopback address may " +
                    "cause the server to leave lockdown mode");
    registerMessage(MSGID_TASK_DISCONNECT_NO_PRIVILEGE,
                    "You do not have sufficient privileges to terminate " +
                    "client connections");
    registerMessage(MSGID_TASK_DISCONNECT_INVALID_CONN_ID,
                    "Unable to decode value %s as an integer connection ID");
    registerMessage(MSGID_TASK_DISCONNECT_NO_CONN_ID,
                    "Attribute %s must be provided to specify the connection " +
                    "ID for the client to disconnect");
    registerMessage(MSGID_TASK_DISCONNECT_INVALID_NOTIFY_CLIENT,
                    "Unable to decode value %s as an indication of whether " +
                    "to notify the client before disconnecting it.  The " +
                    "provided value should be either 'true' or 'false'");
    registerMessage(MSGID_TASK_DISCONNECT_GENERIC_MESSAGE,
                    "An administrator has terminated this client connection");
    registerMessage(MSGID_TASK_DISCONNECT_NO_SUCH_CONNECTION,
                    "There is no client connection with connection ID %s");
    registerMessage(MSGID_TASK_DISCONNECT_MESSAGE,
                    "An administrator has terminated this client connection");
  }
}
opends/src/server/org/opends/server/tasks/DisconnectClientTask.java
New file
@@ -0,0 +1,236 @@
/*
 * 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
 *
 *
 *      Portions Copyright 2007 Sun Microsystems, Inc.
 */
package org.opends.server.tasks;
import java.util.List;
import org.opends.server.admin.std.server.ConnectionHandlerCfg;
import org.opends.server.backends.task.Task;
import org.opends.server.backends.task.TaskState;
import org.opends.server.api.ClientConnection;
import org.opends.server.api.ConnectionHandler;
import org.opends.server.core.DirectoryServer;
import org.opends.server.types.Attribute;
import org.opends.server.types.AttributeType;
import org.opends.server.types.AttributeValue;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.DisconnectReason;
import org.opends.server.types.Entry;
import org.opends.server.types.ErrorLogCategory;
import org.opends.server.types.ErrorLogSeverity;
import org.opends.server.types.Operation;
import org.opends.server.types.Privilege;
import org.opends.server.types.ResultCode;
import static org.opends.server.messages.MessageHandler.*;
import static org.opends.server.messages.TaskMessages.*;
import static org.opends.server.util.ServerConstants.*;
import static org.opends.server.util.StaticUtils.*;
/**
 * This class provides an implementation of a Directory Server task that can be
 * used to terminate a client connection.
 */
public class DisconnectClientTask
       extends Task
{
  // Indicates whether to send a notification message to the client.
  private boolean notifyClient;
  // The connection ID for the client connection to terminate.
  private long connectionID;
  // The disconnect message to send to the client.
  private String disconnectMessage;
  /**
   * {@inheritDoc}
   */
  @Override
  public void initializeTask()
         throws DirectoryException
  {
    // If the client connection is available, then make sure the client has the
    // DISCONNECT_CLIENT privilege.
    Operation operation = getOperation();
    if (operation != null)
    {
      ClientConnection conn = operation.getClientConnection();
      if (! conn.hasPrivilege(Privilege.DISCONNECT_CLIENT, operation))
      {
        int    msgID   = MSGID_TASK_DISCONNECT_NO_PRIVILEGE;
        String message = getMessage(msgID);
        throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS,
                                     message, msgID);
      }
    }
    // Get the connection ID for the client connection.
    Entry taskEntry = getTaskEntry();
    connectionID = -1L;
    AttributeType attrType =
         DirectoryServer.getAttributeType(ATTR_TASK_DISCONNECT_CONN_ID, true);
    List<Attribute> attrList = taskEntry.getAttribute(attrType);
    if (attrList != null)
    {
connIDLoop:
      for (Attribute a : attrList)
      {
        for (AttributeValue v : a.getValues())
        {
          try
          {
            connectionID = Long.parseLong(v.getStringValue());
            break connIDLoop;
          }
          catch (Exception e)
          {
            int    msgID   = MSGID_TASK_DISCONNECT_INVALID_CONN_ID;
            String message = getMessage(msgID, v.getStringValue());
            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                         message, msgID, e);
          }
        }
      }
    }
    if (connectionID < 0)
    {
      int    msgID   = MSGID_TASK_DISCONNECT_NO_CONN_ID;
      String message = getMessage(msgID, ATTR_TASK_DISCONNECT_CONN_ID);
      throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION,
                                   message, msgID);
    }
    // Determine whether to notify the client.
    notifyClient = false;
    attrType =
         DirectoryServer.getAttributeType(ATTR_TASK_DISCONNECT_NOTIFY_CLIENT,
                                          true);
    attrList = taskEntry.getAttribute(attrType);
    if (attrList != null)
    {
notifyClientLoop:
      for (Attribute a : attrList)
      {
        for (AttributeValue v : a.getValues())
        {
          String stringValue = toLowerCase(v.getStringValue());
          if (stringValue.equals("true"))
          {
            notifyClient = true;
            break notifyClientLoop;
          }
          else if (stringValue.equals("false"))
          {
            break notifyClientLoop;
          }
          else
          {
            int    msgID   = MSGID_TASK_DISCONNECT_INVALID_NOTIFY_CLIENT;
            String message = getMessage(msgID, stringValue);
            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                         message, msgID);
          }
        }
      }
    }
    // Get the disconnect message.
    disconnectMessage = getMessage(MSGID_TASK_DISCONNECT_GENERIC_MESSAGE);
    attrType = DirectoryServer.getAttributeType(ATTR_TASK_DISCONNECT_MESSAGE,
                                                true);
    attrList = taskEntry.getAttribute(attrType);
    if (attrList != null)
    {
disconnectMessageLoop:
      for (Attribute a : attrList)
      {
        for (AttributeValue v : a.getValues())
        {
          disconnectMessage = v.getStringValue();
          break disconnectMessageLoop;
        }
      }
    }
  }
  /**
   * {@inheritDoc}
   */
  protected TaskState runTask()
  {
    // Get the specified client connection.
    ClientConnection clientConnection = null;
    for (ConnectionHandler handler : DirectoryServer.getConnectionHandlers())
    {
      ConnectionHandler<? extends ConnectionHandlerCfg> connHandler =
           (ConnectionHandler<? extends ConnectionHandlerCfg>) handler;
      for (ClientConnection c : connHandler.getClientConnections())
      {
        if (c.getConnectionID() == connectionID)
        {
          clientConnection = c;
          break;
        }
      }
    }
    // If there is no such client connection, then return an error.  Otherwise,
    // terminate it.
    if (clientConnection == null)
    {
      int    msgID   = MSGID_TASK_DISCONNECT_NO_SUCH_CONNECTION;
      String message = getMessage(msgID, connectionID);
      logError(ErrorLogCategory.TASK, ErrorLogSeverity.SEVERE_ERROR, message,
               msgID);
      return TaskState.COMPLETED_WITH_ERRORS;
    }
    else
    {
      clientConnection.disconnect(DisconnectReason.ADMIN_DISCONNECT,
                                  notifyClient, disconnectMessage,
                                  MSGID_TASK_DISCONNECT_MESSAGE);
      return TaskState.COMPLETED_SUCCESSFULLY;
    }
  }
}
opends/src/server/org/opends/server/util/ServerConstants.java
@@ -498,6 +498,32 @@
  /**
   * The name of the attribute that is used to specify the connection ID of the
   * connection to disconnect.
   */
  public static final String ATTR_TASK_DISCONNECT_CONN_ID =
       "ds-task-disconnect-connection-id";
  /**
   * The name of the attribute that is used to specify the disconnect message.
   */
  public static final String ATTR_TASK_DISCONNECT_MESSAGE =
       "ds-task-disconnect-message";
  /**
   * The name of the attribute that is used to indicate whether to notify the
   * connection it is about to be terminated.
   */
  public static final String ATTR_TASK_DISCONNECT_NOTIFY_CLIENT =
       "ds-task-disconnect-notify-client";
  /**
   * The name of the attribute that is used to specify the total number of
   * connections established since startup, formatted in camel case.
   */
@@ -687,6 +713,15 @@
  /**
   * The OID for the extended operation that can be used to get the client
   * connection ID.  It will be both the request and response OID.
   */
  public static final String OID_GET_CONNECTION_ID_EXTOP =
       "1.3.6.1.4.1.26027.1.6.2";
  /**
   * The request OID for the password modify extended operation.
   */
  public static final String OID_PASSWORD_MODIFY_REQUEST =
opends/tests/unit-tests-testng/src/server/org/opends/server/tasks/DisconnectClientTaskTestCase.java
New file
@@ -0,0 +1,271 @@
/*
 * 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
 *
 *
 *      Portions Copyright 2007 Sun Microsystems, Inc.
 */
package org.opends.server.tasks;
import java.net.Socket;
import org.testng.annotations.Test;
import org.testng.annotations.BeforeClass;
import org.opends.server.TestCaseUtils;
import org.opends.server.backends.task.Task;
import org.opends.server.backends.task.TaskBackend;
import org.opends.server.backends.task.TaskState;
import org.opends.server.core.DirectoryServer;
import org.opends.server.extensions.GetConnectionIDExtendedOperation;
import org.opends.server.protocols.asn1.*;
import org.opends.server.protocols.ldap.*;
import org.opends.server.types.DN;
import static org.testng.Assert.*;
import static org.opends.server.util.ServerConstants.*;
/**
 * Tests the disconnect client task.
 */
public class DisconnectClientTaskTestCase
       extends TasksTestCase
{
  /**
   * Make sure that the Directory Server is running.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @BeforeClass()
  public void startServer()
         throws Exception
  {
    TestCaseUtils.startServer();
  }
  /**
   * Tests the ability of the server to disconnect an arbitrary client
   * connection with a notice of disconnection.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test()
  public void testDisconnectWithNotification()
         throws Exception
  {
    // Establish a connection to the server, bind, and get the connection ID.
    Socket s = new Socket("127.0.0.1", TestCaseUtils.getServerLdapPort());
    ASN1Reader r = new ASN1Reader(s);
    ASN1Writer w = new ASN1Writer(s);
    BindRequestProtocolOp bindRequest =
         new BindRequestProtocolOp(new ASN1OctetString("cn=Directory Manager"),
                                   3, new ASN1OctetString("password"));
    LDAPMessage message = new LDAPMessage(1, bindRequest);
    w.writeElement(message.encode());
    message = LDAPMessage.decode(r.readElement().decodeAsSequence());
    BindResponseProtocolOp bindResponse = message.getBindResponseProtocolOp();
    assertEquals(bindResponse.getResultCode(), LDAPResultCode.SUCCESS);
    ExtendedRequestProtocolOp extendedRequest =
         new ExtendedRequestProtocolOp(OID_GET_CONNECTION_ID_EXTOP);
    message = new LDAPMessage(2, extendedRequest);
    w.writeElement(message.encode());
    message = LDAPMessage.decode(r.readElement().decodeAsSequence());
    ExtendedResponseProtocolOp extendedResponse =
         message.getExtendedResponseProtocolOp();
    assertEquals(extendedResponse.getResultCode(), LDAPResultCode.SUCCESS);
    assertEquals(extendedResponse.getOID(), OID_GET_CONNECTION_ID_EXTOP);
    long connectionID = GetConnectionIDExtendedOperation.decodeResponseValue(
                             extendedResponse.getValue());
    // Invoke the disconnect client task.
    String taskID = "Disconnect Client " + connectionID;
    String disconnectMessage = "testDisconnectWithNotification";
    DN taskDN = DN.decode("ds-task-id=" + taskID +
                          ",cn=Scheduled Tasks,cn=Tasks");
    TestCaseUtils.addEntry(
      "dn: " + taskDN.toString(),
      "objectClass: top",
      "objectClass: ds-task",
      "objectClass: ds-task-disconnect",
      "ds-task-id: " + taskID,
      "ds-task-class-name: org.opends.server.tasks.DisconnectClientTask",
      "ds-task-disconnect-connection-id: " + connectionID,
      "ds-task-disconnect-notify-client: true",
      "ds-task-disconnect-message: " + disconnectMessage);
    Task task = getCompletedTask(taskDN);
    assertNotNull(task);
    assertEquals(task.getTaskState(), TaskState.COMPLETED_SUCCESSFULLY);
    // Make sure that we get a notice of disconnection on the initial
    // connection.
    message = LDAPMessage.decode(r.readElement().decodeAsSequence());
    extendedResponse = message.getExtendedResponseProtocolOp();
    assertEquals(extendedResponse.getOID(),
                 LDAPConstants.OID_NOTICE_OF_DISCONNECTION);
    assertEquals(extendedResponse.getErrorMessage(), disconnectMessage);
    try
    {
      s.close();
    } catch (Exception e) {}
  }
  /**
   * Tests the ability of the server to disconnect an arbitrary client
   * connection without a notice of disconnection.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test()
  public void testDisconnectWithoutNotification()
         throws Exception
  {
    // Establish a connection to the server, bind, and get the connection ID.
    Socket s = new Socket("127.0.0.1", TestCaseUtils.getServerLdapPort());
    ASN1Reader r = new ASN1Reader(s);
    ASN1Writer w = new ASN1Writer(s);
    BindRequestProtocolOp bindRequest =
         new BindRequestProtocolOp(new ASN1OctetString("cn=Directory Manager"),
                                   3, new ASN1OctetString("password"));
    LDAPMessage message = new LDAPMessage(1, bindRequest);
    w.writeElement(message.encode());
    message = LDAPMessage.decode(r.readElement().decodeAsSequence());
    BindResponseProtocolOp bindResponse = message.getBindResponseProtocolOp();
    assertEquals(bindResponse.getResultCode(), LDAPResultCode.SUCCESS);
    ExtendedRequestProtocolOp extendedRequest =
         new ExtendedRequestProtocolOp(OID_GET_CONNECTION_ID_EXTOP);
    message = new LDAPMessage(2, extendedRequest);
    w.writeElement(message.encode());
    message = LDAPMessage.decode(r.readElement().decodeAsSequence());
    ExtendedResponseProtocolOp extendedResponse =
         message.getExtendedResponseProtocolOp();
    assertEquals(extendedResponse.getResultCode(), LDAPResultCode.SUCCESS);
    assertEquals(extendedResponse.getOID(), OID_GET_CONNECTION_ID_EXTOP);
    long connectionID = GetConnectionIDExtendedOperation.decodeResponseValue(
                             extendedResponse.getValue());
    // Invoke the disconnect client task.
    String taskID = "Disconnect Client " + connectionID;
    DN taskDN = DN.decode("ds-task-id=" + taskID +
                          ",cn=Scheduled Tasks,cn=Tasks");
    TestCaseUtils.addEntry(
      "dn: " + taskDN.toString(),
      "objectClass: top",
      "objectClass: ds-task",
      "objectClass: ds-task-disconnect",
      "ds-task-id: " + taskID,
      "ds-task-class-name: org.opends.server.tasks.DisconnectClientTask",
      "ds-task-disconnect-connection-id: " + connectionID,
      "ds-task-disconnect-notify-client: false");
    Task task = getCompletedTask(taskDN);
    assertNotNull(task);
    assertEquals(task.getTaskState(), TaskState.COMPLETED_SUCCESSFULLY);
    // Make sure that the client connection has been closed with no notice of
    // disconnection.
    assertNull(r.readElement());
    try
    {
      s.close();
    } catch (Exception e) {}
  }
  /**
   * Retrieves the specified task from the server, waiting for it to finish all
   * the running its going to do before returning.
   *
   * @param  taskEntryDN  The DN of the entry for the task to retrieve.
   *
   * @return  The requested task entry.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  private Task getCompletedTask(DN taskEntryDN)
          throws Exception
  {
    TaskBackend taskBackend =
         (TaskBackend) DirectoryServer.getBackend(DN.decode("cn=tasks"));
    Task task = taskBackend.getScheduledTask(taskEntryDN);
    if (task == null)
    {
      long stopWaitingTime = System.currentTimeMillis() + 10000L;
      while ((task == null) && (System.currentTimeMillis() < stopWaitingTime))
      {
        Thread.sleep(10);
        task = taskBackend.getScheduledTask(taskEntryDN);
      }
    }
    if (task == null)
    {
      throw new AssertionError("There is no such task " +
                               taskEntryDN.toString());
    }
    if (! TaskState.isDone(task.getTaskState()))
    {
      long stopWaitingTime = System.currentTimeMillis() + 20000L;
      while ((! TaskState.isDone(task.getTaskState())) &&
             (System.currentTimeMillis() < stopWaitingTime))
      {
        Thread.sleep(10);
      }
    }
    if (! TaskState.isDone(task.getTaskState()))
    {
      throw new AssertionError("Task " + taskEntryDN.toString() +
                               " did not complete in a timely manner.");
    }
    return task;
  }
}