/*
 * 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 2007-2010 Sun Microsystems, Inc.
 *      Portions Copyright 2011-2015 ForgeRock AS
 *      Portions Copyrighted 2019 OGIS-RI Co., Ltd.
 */
package org.opends.server.types;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;

import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.ModificationType;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.SearchScope;
import org.opends.server.TestCaseUtils;
import org.opends.server.admin.std.meta.GlobalCfgDefn.DisabledPrivilege;
import org.opends.server.admin.std.meta.RootDNCfgDefn;
import org.opends.server.api.ClientConnection;
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.controls.ProxiedAuthV1Control;
import org.opends.server.controls.ProxiedAuthV2Control;
import org.opends.server.core.AddOperation;
import org.opends.server.core.AddOperationBasis;
import org.opends.server.core.CompareOperation;
import org.opends.server.core.CompareOperationBasis;
import org.opends.server.core.DeleteOperation;
import org.opends.server.core.DeleteOperationBasis;
import org.opends.server.core.DirectoryServer;
import org.opends.server.core.ModifyDNOperation;
import org.opends.server.core.ModifyDNOperationBasis;
import org.opends.server.core.ModifyOperation;
import org.opends.server.core.ModifyOperationBasis;
import org.opends.server.core.SchemaConfigManager;
import org.opends.server.protocols.internal.InternalClientConnection;
import org.opends.server.protocols.internal.InternalSearchOperation;
import org.opends.server.protocols.internal.Requests;
import org.opends.server.protocols.internal.SearchRequest;
import org.opends.server.protocols.ldap.BindRequestProtocolOp;
import org.opends.server.protocols.ldap.BindResponseProtocolOp;
import org.opends.server.protocols.ldap.LDAPMessage;
import org.opends.server.tools.LDAPModify;
import org.opends.server.tools.LDAPPasswordModify;
import org.opends.server.tools.LDAPReader;
import org.opends.server.tools.LDAPSearch;
import org.opends.server.tools.LDAPWriter;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import static org.forgerock.opendj.ldap.ResultCode.*;
import static org.opends.server.TestCaseUtils.*;
import static org.opends.server.protocols.internal.InternalClientConnection.*;
import static org.opends.server.protocols.internal.Requests.*;
import static org.testng.Assert.*;
import org.testng.Assert;

/**
 * This class provides a set of test cases for the Directory Server privilege
 * subsystem.
 *
 * FIXME -- It will likely be necessary to also have access control rules in
 *          place to allow operations as necessary once that functionality has
 *          integrated into the server.
 */
public class PrivilegeTestCase extends TypesTestCase
{

  /**
   * The DN of the user that is associated with the internal root connection.
   */
  private static final String INTERNAL_ROOT_DN =
       "cn=Internal Client,cn=Root DNs,cn=config";



  /**
   * A Map of client connections that should be used when performing operations
   * and whether config read operations should be successful.
   */
  private Map<InternalClientConnection, Boolean> connections = new HashMap<>();

  /**
   * Make sure that the server is running and that an appropriate set of
   * structures are in place.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @BeforeClass
  public void setUp()
         throws Exception
  {
    TestCaseUtils.startServer();
    TestCaseUtils.enableBackend("unindexedRoot");

    TestCaseUtils.dsconfig(
            "set-sasl-mechanism-handler-prop",
            "--handler-name", "DIGEST-MD5",
            "--set", "server-fqdn:" + "127.0.0.1");

    TestCaseUtils.initializeTestBackend(true);
    TestCaseUtils.addEntries(
      "dn: cn=Unprivileged Root,cn=Root DNs,cn=config",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "objectClass: ds-cfg-root-dn-user",
      "cn: Unprivileged Root",
      "givenName: Unprivileged",
      "sn: Root",
      "uid: unprivileged.root",
      "userPassword: password",
      "ds-privilege-name: -config-read",
      "ds-privilege-name: -config-write",
      "ds-privilege-name: -password-reset",
      "ds-privilege-name: -update-schema",
      "ds-privilege-name: -ldif-import",
      "ds-privilege-name: -ldif-export",
      "ds-privilege-name: -backend-backup",
      "ds-privilege-name: -backend-restore",
      "ds-privilege-name: -unindexed-search",
      "ds-privilege-name: -subentry-write",
      "",
      "dn: cn=Proxy Root,cn=Root DNs,cn=config",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "objectClass: ds-cfg-root-dn-user",
      "cn: Proxy Root",
      "givenName: Proxy",
      "sn: Root",
      "uid: proxy.root",
      "userPassword: password",
      "ds-privilege-name: proxied-auth",
      "",
      "dn: cn=Privileged User,o=test",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "cn: Privileged User",
      "givenName: Privileged",
      "sn: User",
      "uid: privileged.user",
      "userPassword: password",
      "ds-privilege-name: config-read",
      "ds-privilege-name: config-write",
      "ds-privilege-name: password-reset",
      "ds-privilege-name: update-schema",
      "ds-privilege-name: ldif-import",
      "ds-privilege-name: ldif-export",
      "ds-privilege-name: backend-backup",
      "ds-privilege-name: backend-restore",
      "ds-privilege-name: proxied-auth",
      "ds-privilege-name: bypass-acl",
      "ds-privilege-name: unindexed-search",
      "ds-privilege-name: subentry-write",
      "ds-pwp-password-policy-dn: cn=Clear UserPassword Policy," +
           "cn=Password Policies,cn=config",
      "",
      "dn: cn=Unprivileged User,o=test",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "cn: Unprivileged User",
      "givenName: Unprivileged",
      "sn: User",
      "uid: unprivileged.user",
      "ds-privilege-name: bypass-acl",
      "userPassword: password",
      "ds-pwp-password-policy-dn: cn=Clear UserPassword Policy," +
           "cn=Password Policies,cn=config",
      "",
      "dn: cn=Subentry Target,o=test",
      "objectClass: top",
      "objectClass: subentry",
      "objectClass: collectiveAttributeSubentry",
      "objectClass: extensibleObject",
      "cn: Subentry Target",
      "l;collective: Test",
      "subtreeSpecification: {}",
      "",
      "dn: cn=PWReset Target,o=test",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "cn: PWReset Target",
      "givenName: PWReset",
      "sn: Target",
      "uid: pwreset.target",
      "userPassword: password");

    TestCaseUtils.applyModifications(false,
      "dn: o=test",
      "changetype: modify",
      "add: aci",
      "aci: (version 3.0; acl \"Proxy Root\"; allow (proxy) " +
           "userdn=\"ldap:///cn=Proxy Root,cn=Root DNs,cn=config\";)",
      "aci: (version 3.0; acl \"Unprivileged Root\"; allow (proxy) " +
           "userdn=\"ldap:///cn=Unprivileged Root,cn=Root DNs,cn=config\";)",
      "aci: (version 3.0; acl \"Privileged User\"; allow (proxy) " +
           "userdn=\"ldap:///cn=Privileged User,o=test\";)",
      "aci: (targetattr=\"*\")(version 3.0; acl \"PWReset Target\"; " +
           "allow (all) userdn=\"ldap:///cn=PWReset Target,o=test\";)");


    // Build the array of connections we will use to perform the tests.
    connections.put(new InternalClientConnection(new AuthenticationInfo()), false);
    connections.put(InternalClientConnection.getRootConnection(), true);
    connections.put(newConn("cn=Directory Manager,cn=Root DNs,cn=config", true), true);
    connections.put(newConn("cn=Unprivileged Root,cn=Root DNs,cn=config", true), false);
    connections.put(newConn("cn=Proxy Root,cn=Root DNs,cn=config", true), true);
    connections.put(newConn("cn=Unprivileged User,o=test", false), false);
    connections.put(newConn("cn=Privileged User,o=test", false), true);


    TestCaseUtils.addEntries(
        "dn: dc=unindexed,dc=jeb",
        "objectClass: top",
        "objectClass: domain",
        "",
        "dn: cn=test1 user,dc=unindexed,dc=jeb",
        "objectClass: top",
        "objectClass: person",
        "objectClass: organizationalPerson",
        "objectClass: inetOrgPerson",
        "cn: test1 user",
        "givenName: user",
        "sn: test1",
        "carLicense: test1",
        "",
        "dn: cn=test2 user,dc=unindexed,dc=jeb",
        "objectClass: top",
        "objectClass: person",
        "objectClass: organizationalPerson",
        "objectClass: inetOrgPerson",
        "cn: test2 user",
        "givenName: user",
        "sn: test2",
        "carLicense: test2"
    );
    for (int i = 0; i < 5000; i++)
    {
      String userNb = "user." + i;
      TestCaseUtils.addEntry(
          "dn: cn=" + userNb + ",dc=unindexed,dc=jeb",
          "objectClass: top",
          "objectClass: person",
          "objectClass: organizationalPerson",
          "objectClass: inetOrgPerson",
          "cn: " + userNb,
          "givenName: " + userNb,
          "sn: " + userNb,
          "carLicense: " + userNb
      );
    }
  }

  private InternalClientConnection newConn(String userDN,
      boolean isRoot)
      throws DirectoryException
  {
    Entry userEntry = DirectoryServer.getEntry(DN.valueOf(userDN));
    AuthenticationInfo authInfo = new AuthenticationInfo(userEntry, isRoot);
    return new InternalClientConnection(authInfo);
  }



  /**
   * Cleans up anything that might be left around after running the tests in
   * this class.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @AfterClass
  public void cleanUp() throws Exception
  {
    TestCaseUtils.dsconfig(
            "set-sasl-mechanism-handler-prop",
            "--handler-name", "DIGEST-MD5",
            "--remove", "server-fqdn:" + "127.0.0.1");

    assertDeleteSuccessfully("cn=Unprivileged Root,cn=Root DNs,cn=config");
    assertDeleteSuccessfully("cn=Proxy Root,cn=Root DNs,cn=config");
    assertDeleteSuccessfully("cn=Privileged User,o=test");
    assertDeleteSuccessfully("cn=UnPrivileged User,o=test");
    assertDeleteSuccessfully("cn=PWReset Target,o=test");
    assertDeleteSuccessfully("cn=test1 user,dc=unindexed,dc=jeb");
    assertDeleteSuccessfully("cn=test2 user,dc=unindexed,dc=jeb");
    for (int i = 0; i < 5000; i++)
    {
      assertDeleteSuccessfully("cn=user." + i + ",dc=unindexed,dc=jeb");
    }
    assertDeleteSuccessfully("dc=unindexed,dc=jeb");

    TestCaseUtils.disableBackend("unindexedRoot");
  }

  private void assertDeleteSuccessfully(String dn) throws DirectoryException
  {
    DeleteOperation deleteOperation = getRootConnection().processDelete(dn);
    assertEquals(deleteOperation.getResultCode(), ResultCode.SUCCESS);
  }



  /**
   * Retrieves a set of data that can be used for performing the tests.  The
   * arguments generated for each method will be:
   * <OL>
   *   <LI>A client connection to use to perform the operation</LI>
   *   <LI>A flag indicating whether or not the operation should succeed</LI>
   * </OL>
   *
   * @return  A set of data that can be used for performing the tests.
   */
  @DataProvider(name = "testdata")
  public Object[][] getTestData()
  {
    Object[][] returnArray = new Object[connections.size()][2];
    int i = 0;
    for (Map.Entry<InternalClientConnection, Boolean> entry : connections
        .entrySet())
    {
      returnArray[i][0] = entry.getKey();
      returnArray[i][1] = entry.getValue();
      i++;
    }

    return returnArray;
  }



  /**
   * Tests to ensure that search operations in the server configuration properly
   * respect the CONFIG_READ privilege.
   *
   * @param  conn          The client connection to use to perform the search
   *                       operation.
   * @param  hasPrivilege  Indicates whether the authenticated user is expected
   *                       to have the CONFIG_READ privilege and therefore the
   *                       search should succeed.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test(dataProvider = "testdata")
  public void testConfigReadSearch(InternalClientConnection conn,
                                   boolean hasPrivilege)
         throws Exception
  {
    assertEquals(conn.hasPrivilege(Privilege.CONFIG_READ, null), hasPrivilege);

    SearchRequest request = Requests.newSearchRequest(DN.valueOf("cn=config"), SearchScope.BASE_OBJECT);
    InternalSearchOperation searchOperation = conn.processSearch(request);
    assertPrivilege(searchOperation.getResultCode(), hasPrivilege);
  }

  /**
   * Tests to ensure that unindexed search operations properly respect the
   * UNINDEXED_SEARCH privilege.
   *
   * @param conn The client connection to use to perform the search operation.
   *
   * @param hasPrivilege Indicates whether the authenticated user is expected
   *                     to have the UNINDEXED_SEARCH privilege and therefore
   *                     the search should succeed.
   * @throws Exception If an unexpected problem occurs.
   */
  @Test(dataProvider = "testdata")
  public void testUnindexedSearch(InternalClientConnection conn,
                                  boolean hasPrivilege)
         throws Exception
  {
    assertEquals(conn.hasPrivilege(Privilege.UNINDEXED_SEARCH, null), hasPrivilege);

    SearchRequest request = newSearchRequest("dc=unindexed,dc=jeb", SearchScope.WHOLE_SUBTREE, "(carLicense=test*)");
    InternalSearchOperation searchOperation = conn.processSearch(request);
    assertPrivilege(searchOperation.getResultCode(), hasPrivilege);
  }

  private void assertPrivilege(ResultCode actual, boolean hasPrivilege)
  {
    assertEquals(actual, hasPrivilege ? SUCCESS : INSUFFICIENT_ACCESS_RIGHTS);
  }

  private void assertProxyPrivilege(ResultCode actual, boolean hasProxyPrivilege)
  {
    assertEquals(actual, hasProxyPrivilege ? SUCCESS : AUTHORIZATION_DENIED);
  }

  /**
   * Tests to ensure that compare operations in the server configuration
   * properly respect the CONFIG_READ privilege.
   *
   * @param  conn          The client connection to use to perform the compare
   *                       operation.
   * @param  hasPrivilege  Indicates whether the authenticated user is expected
   *                       to have the CONFIG_READ privilege and therefore the
   *                       compare should succeed.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test(dataProvider = "testdata")
  public void testConfigReadCompare(InternalClientConnection conn,
                                    boolean hasPrivilege)
         throws Exception
  {
    assertEquals(conn.hasPrivilege(Privilege.CONFIG_READ, null), hasPrivilege);

    AttributeType cnAttrType = DirectoryServer.getAttributeTypeOrNull("cn");
    CompareOperation compareOperation = conn.processCompare(
        DN.valueOf("cn=config"), cnAttrType, ByteString.valueOfUtf8("config"));
    if (hasPrivilege)
    {
      assertEquals(compareOperation.getResultCode(), COMPARE_TRUE);
    }
    else
    {
      assertEquals(compareOperation.getResultCode(), INSUFFICIENT_ACCESS_RIGHTS);
    }
  }



  /**
   * Tests to ensure that add and delete operations in the server configuration
   * properly respect the CONFIG_WRITE privilege.
   *
   * @param  conn          The client connection to use to perform the
   *                       operations.
   * @param  hasPrivilege  Indicates whether the authenticated user is expected
   *                       to have the CONFIG_WRITE privilege and therefore the
   *                       operations should succeed.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test(dataProvider = "testdata")
  public void testConfigWriteAddAndDelete(InternalClientConnection conn,
                                          boolean hasPrivilege)
         throws Exception
  {
    assertEquals(conn.hasPrivilege(Privilege.CONFIG_WRITE, null), hasPrivilege);

    Entry entry = TestCaseUtils.makeEntry(
      "dn: cn=Test Root,cn=Root DNs,cn=config",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "objectClass: ds-cfg-root-dn-user",
      "cn: Test Root",
      "givenName: Test",
      "sn: Root",
      "userPassword: password");

    AddOperation addOperation = conn.processAdd(entry);
    assertPrivilege(addOperation.getResultCode(), hasPrivilege);

    DN dnToRemove = entry.getName();
    if (!hasPrivilege)
    {
      dnToRemove = DN.valueOf("cn=Telex Number,cn=Syntaxes,cn=config");
    }
    DeleteOperation deleteOperation = conn.processDelete(dnToRemove);
    assertPrivilege(deleteOperation.getResultCode(), hasPrivilege);
  }



  /**
   * Tests to ensure that modify operations in the server configuration
   * properly respect the CONFIG_WRITE privilege.
   *
   * @param  conn          The client connection to use to perform the modify
   *                       operation.
   * @param  hasPrivilege  Indicates whether the authenticated user is expected
   *                       to have the CONFIG_WRITE privilege and therefore the
   *                       modify should succeed.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test(dataProvider = "testdata")
  public void testConfigWriteModify(InternalClientConnection conn,
                                    boolean hasPrivilege)
         throws Exception
  {
    assertEquals(conn.hasPrivilege(Privilege.CONFIG_WRITE, null), hasPrivilege);

    List<Modification> mods = new ArrayList<>();
    mods.add(new Modification(ModificationType.REPLACE,
                              Attributes.create("ds-cfg-size-limit", "2000")));

    ModifyOperation modifyOperation =
         conn.processModify(DN.valueOf("cn=config"), mods);
    assertPrivilege(modifyOperation.getResultCode(), hasPrivilege);

    if (hasPrivilege)
    {
      mods.clear();
      mods.add(new Modification(ModificationType.REPLACE,
          Attributes.create("ds-cfg-size-limit", "1000")));

      modifyOperation = conn.processModify(DN.valueOf("cn=config"), mods);
      assertEquals(modifyOperation.getResultCode(), ResultCode.SUCCESS);
    }
  }



  /**
   * Tests to ensure that modify DN operations in the server configuration
   * properly respect the CONFIG_WRITE privilege.
   *
   * @param  conn          The client connection to use to perform the modify DN
   *                       operation.
   * @param  hasPrivilege  Indicates whether the authenticated user is expected
   *                       to have the CONFIG_WRITE privilege and therefore the
   *                       modify DN should succeed (or at least get past the
   *                       privilege check, only to fail because we don't
   *                       support modify DN in the server configuration).
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test(dataProvider = "testdata")
  public void testConfigWriteModifyDN(InternalClientConnection conn,
                                      boolean hasPrivilege)
         throws Exception
  {
    assertEquals(conn.hasPrivilege(Privilege.CONFIG_WRITE, null), hasPrivilege);

    ModifyDNOperation modifyDNOperation =
         conn.processModifyDN(DN.valueOf("cn=Work Queue,cn=config"),
                              RDN.decode("cn=New RDN for Work Queue"), true,
                              null);
    if (hasPrivilege)
    {
      // We don't support modify DN operations in the server configuration, but
      // at least we need to make sure we're getting past the privilege check.
      assertEquals(modifyDNOperation.getResultCode(), UNWILLING_TO_PERFORM);
    }
    else
    {
      assertEquals(modifyDNOperation.getResultCode(), INSUFFICIENT_ACCESS_RIGHTS);
    }
  }



  /**
   * Tests to ensure that add and delete operations
   * properly respect the SUBENTRY_WRITE privilege.
   *
   * @param  conn          The client connection to use to perform the
   *                       operations.
   * @param  hasPrivilege  Indicates whether the authenticated user is expected
   *                       to have the SUBENTRY_WRITE privilege and therefore
   *                       the operations should succeed.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test(dataProvider = "testdata")
  public void testSubentryWriteAddAndDelete(InternalClientConnection conn,
                                          boolean hasPrivilege)
         throws Exception
  {
    assertEquals(conn.hasPrivilege(Privilege.SUBENTRY_WRITE, null),
            hasPrivilege);

    Entry entry = TestCaseUtils.makeEntry(
      "dn: cn=Test Subentry,o=test",
      "objectClass: top",
      "objectClass: subentry",
      "objectClass: collectiveAttributeSubentry",
      "objectClass: extensibleObject",
      "cn: Test Subentry",
      "l;collective: Test",
      "subtreeSpecification: {}");

    AddOperation addOperation = conn.processAdd(entry);
    assertPrivilege(addOperation.getResultCode(), hasPrivilege);

    DN dnToRemove = entry.getName();
    if (!hasPrivilege)
    {
      dnToRemove = DN.valueOf("cn=Subentry Target,o=test");
    }
    DeleteOperation deleteOperation = conn.processDelete(dnToRemove);
    assertPrivilege(deleteOperation.getResultCode(), hasPrivilege);
  }



  /**
   * Tests to ensure that modify operations properly respect
   * the SUBENTRY_WRITE privilege.
   *
   * @param  conn          The client connection to use to perform the modify
   *                       operation.
   * @param  hasPrivilege  Indicates whether the authenticated user is expected
   *                       to have the SUBENTRY_WRITE privilege and therefore
   *                       the modify should succeed.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test(dataProvider = "testdata")
  public void testSubentryWriteModify(InternalClientConnection conn,
                                    boolean hasPrivilege)
         throws Exception
  {
    assertEquals(conn.hasPrivilege(Privilege.SUBENTRY_WRITE, null),
            hasPrivilege);

    List<Modification> mods = new ArrayList<>();
    mods.add(new Modification(ModificationType.REPLACE,
                              Attributes.create("subtreeSpecification",
                              "{base \"ou=doesnotexist\"}")));

    ModifyOperation modifyOperation =
         conn.processModify(DN.valueOf("cn=Subentry Target,o=test"), mods);
    assertPrivilege(modifyOperation.getResultCode(), hasPrivilege);

    if (hasPrivilege)
    {
      mods.clear();
      mods.add(new Modification(ModificationType.REPLACE,
          Attributes.create("subtreeSpecification", "{}")));

      modifyOperation = conn.processModify(
              DN.valueOf("cn=Subentry Target,o=test"), mods);
      assertEquals(modifyOperation.getResultCode(), ResultCode.SUCCESS);
    }
  }



  /**
   * Tests to ensure that modify DN operations
   * properly respect the SUBENTRY_WRITE privilege.
   *
   * @param  conn          The client connection to use to perform the modify DN
   *                       operation.
   * @param  hasPrivilege  Indicates whether the authenticated user is expected
   *                       to have the SUBENTRY_WRITE privilege and therefore
   *                       the modify DN should succeed.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test(dataProvider = "testdata")
  public void testSubentryWriteModifyDN(InternalClientConnection conn,
                                      boolean hasPrivilege)
         throws Exception
  {
    assertEquals(conn.hasPrivilege(Privilege.SUBENTRY_WRITE, null),
            hasPrivilege);

    ModifyDNOperation modifyDNOperation =
         conn.processModifyDN(DN.valueOf("cn=Subentry Target,o=test"),
                              RDN.decode("cn=New Subentry Target"),
                              true, null);
    assertPrivilege(modifyDNOperation.getResultCode(), hasPrivilege);
    if (hasPrivilege)
    {
      modifyDNOperation =
         conn.processModifyDN(DN.valueOf("cn=New Subentry Target,o=test"),
                              RDN.decode("cn=Subentry Target"),
                              true, null);
      assertEquals(modifyDNOperation.getResultCode(), ResultCode.SUCCESS);
    }
  }



  /**
   * Tests to ensure that modify operations which attempt to reset a user's
   * password properly respect the PASSWORD_RESET privilege.
   *
   * @param  conn          The client connection to use to perform the password
   *                       reset.
   * @param  hasPrivilege  Indicates whether the authenticated user is expected
   *                       to have the PASSWORD_RESET privilege and therefore
   *                       the password reset should succeed.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test(dataProvider = "testdata")
  public void testPasswordResetModify(InternalClientConnection conn,
                                      boolean hasPrivilege)
         throws Exception
  {
    // We've got to do this as an external operation rather than internal, so
    // get the bind DN and password to use from the client connection.

    String userDN;
    String userPassword;
    Entry  authNEntry = conn.getAuthenticationInfo().getAuthenticationEntry();
    if (authNEntry == null)
    {
      userDN       = "";
      userPassword = "";
    }
    else if (authNEntry.getName().toString().equalsIgnoreCase(INTERNAL_ROOT_DN))
    {
      return;
    }
    else
    {
      userDN       = authNEntry.getName().toString();
      userPassword = "password";
    }

    assertEquals(conn.hasPrivilege(Privilege.PASSWORD_RESET, null),
                 hasPrivilege);

    String path = TestCaseUtils.createTempFile(
      "dn: cn=PWReset Target,o=test",
      "changetype: modify",
      "replace: userPassword",
      "userPassword: newpassword",
      "",
      "dn: cn=PWReset Target,o=test",
      "changetype: modify",
      "replace: userPassword",
      "userPassword: password");

    String[] args =
    {
      "--noPropertiesFile",
      "-h", "127.0.0.1",
      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
      "-D", userDN,
      "-w", userPassword,
      "-f", path
    };

    int resultCode = LDAPModify.mainModify(args, false, null, null);
    if (hasPrivilege)
    {
      assertEquals(resultCode, 0);
    }
    else
    {
      assertEquals(resultCode, 50);
    }
  }



  /**
   * Tests to ensure that password modify extended operations which attempt to
   * reset a user's password properly respect the PASSWORD_RESET privilege.
   *
   * @param  conn          The client connection to use to perform the password
   *                       reset.
   * @param  hasPrivilege  Indicates whether the authenticated user is expected
   *                       to have the PASSWORD_RESET privilege and therefore
   *                       the password reset should succeed.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test(dataProvider = "testdata")
  public void testPasswordResetExtOp(InternalClientConnection conn,
                                     boolean hasPrivilege)
         throws Exception
  {
    // We've got to do this as an external operation rather than internal, so
    // get the bind DN and password to use from the client connection.

    String userDN;
    String userPassword;
    Entry  authNEntry = conn.getAuthenticationInfo().getAuthenticationEntry();
    if (authNEntry == null)
    {
      userDN       = "";
      userPassword = "";
    }
    else if (authNEntry.getName().toString().equalsIgnoreCase(INTERNAL_ROOT_DN))
    {
      return;
    }
    else
    {
      userDN       = authNEntry.getName().toString();
      userPassword = "password";
    }

    assertEquals(conn.hasPrivilege(Privilege.PASSWORD_RESET, null),
                 hasPrivilege);

    String[] args =
    {
      "--noPropertiesFile",
      "-h", "127.0.0.1",
      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
      "-D", userDN,
      "-w", userPassword,
      "-a", "dn:cn=PWReset Target,o=test",
      "-n", "newpassword"
    };

    int resultCode =
             LDAPPasswordModify.mainPasswordModify(args, false, null, null);
    if (hasPrivilege)
    {
      assertEquals(resultCode, 0);

      args = new String[]
      {
        "--noPropertiesFile",
        "-h", "127.0.0.1",
        "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
        "-D", userDN,
        "-w", userPassword,
        "-a", "dn:cn=PWReset Target,o=test",
        "-n", "password"
      };
      assertEquals(LDAPPasswordModify.mainPasswordModify(args, false, null,
                                                         null), 0);
    }
    else
    {
      assertEquals(resultCode, 50);
    }
  }



  /**
   * Tests to ensure that attempts to update the schema with a modify operation
   * will properly respect the UPDATE_SCHEMA privilege.
   *
   * @param  conn          The client connection to use to perform the schema
   *                       update.
   * @param  hasPrivilege  Indicates whether the authenticated user is expected
   *                       to have the UPDATE_SCHEMA privilege and therefore
   *                       the schema update should succeed.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test(dataProvider = "testdata")
  public void testUpdateSchemaModify(InternalClientConnection conn,
                               boolean hasPrivilege)
         throws Exception
  {
    assertEquals(conn.hasPrivilege(Privilege.UPDATE_SCHEMA, null),
                 hasPrivilege);

    String attrDefinition =
         "( testupdateschemaat-oid NAME 'testUpdateSchemaAT' " +
         "SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE " +
         "X-ORIGIN 'PrivilegeTestCase' )";

    List<Modification> mods = new ArrayList<>();
    mods.add(new Modification(ModificationType.ADD,
        Attributes.create("attributetypes", attrDefinition)));

    ModifyOperation modifyOperation =
         conn.processModify(DN.valueOf("cn=schema"), mods);
    assertPrivilege(modifyOperation.getResultCode(), hasPrivilege);

    if (hasPrivilege)
    {
      mods.clear();
      mods.add(new Modification(ModificationType.DELETE,
          Attributes.create("attributetypes", attrDefinition)));

      modifyOperation = conn.processModify(DN.valueOf("cn=schema"), mods);
      assertEquals(modifyOperation.getResultCode(), ResultCode.SUCCESS);
    }
  }



  /**
   * Tests to ensure that attempts to update the schema with an add schema file
   * task will properly respect the UPDATE_SCHEMA privilege.
   *
   * @param  conn          The client connection to use to perform the schema
   *                       update.
   * @param  hasPrivilege  Indicates whether the authenticated user is expected
   *                       to have the UPDATE_SCHEMA privilege and therefore
   *                       the schema update should succeed.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test(dataProvider = "testdata")
  public void testUpdateSchemaAddSchemaFile(InternalClientConnection conn,
                                            boolean hasPrivilege)
         throws Exception
  {
    assertEquals(conn.hasPrivilege(Privilege.UPDATE_SCHEMA, null),
                 hasPrivilege);


    String schemaDirectory = SchemaConfigManager.getSchemaDirectoryPath();

    String identifier;
    Entry authNEntry = conn.getAuthenticationInfo().getAuthenticationEntry();
    if (authNEntry == null)
    {
      identifier = "null";
    }
    else
    {
      identifier = authNEntry.getName().toString();
      identifier = identifier.replace(',', '-');
      identifier = identifier.replace(' ', '-');
      identifier = identifier.replace('=', '-');
    }

    String[] fileLines =
    {
      "dn: cn=schema",
      "objectClass: top",
      "objectClass: ldapSubentry",
      "objectClass: subschema",
      "attributeTypes: ( " + identifier.toLowerCase() + "-oid " +
           "NAME '" + identifier + "' )"
    };

    File validFile = new File(schemaDirectory, "05-" + identifier + ".ldif");
    BufferedWriter writer = new BufferedWriter(new FileWriter(validFile));
    for (String line : fileLines)
    {
      writer.write(line);
      writer.newLine();
    }
    writer.close();

    assertPrivilege(conn, hasPrivilege,
      "dn: ds-task-id=" + UUID.randomUUID() + ",cn=Scheduled Tasks,cn=Tasks",
      "objectClass: top",
      "objectClass: ds-task",
      "objectClass: ds-task-add-schema-file",
      "ds-task-class-name: org.opends.server.tasks.AddSchemaFileTask",
      "ds-task-schema-file-name: 05-" + identifier + ".ldif");
  }

  private void assertPrivilege(InternalClientConnection conn, boolean hasPrivilege, String... lines) throws Exception
  {
    Entry taskEntry = TestCaseUtils.makeEntry(lines);

    AddOperation addOperation = conn.processAdd(taskEntry);
    assertPrivilege(addOperation.getResultCode(), hasPrivilege);

    if (hasPrivilege)
    {
      Task task = getCompletedTask(taskEntry.getName());
      assertNotNull(task);
      assertTrue(TaskState.isSuccessful(task.getTaskState()));
    }
  }



  /**
   * Tests to ensure that attempts to backup the Directory Server backends
   * will properly respect the BACKEND_BACKUP privilege.
   *
   * @param  conn          The client connection to use to perform the backup.
   * @param  hasPrivilege  Indicates whether the authenticated user is expected
   *                       to have the BACKEND_BACKUP privilege and therefore
   *                       the backup should succeed.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test(dataProvider = "testdata", groups = { "slow" })
  public void testBackupBackend(InternalClientConnection conn,
                                boolean hasPrivilege)
         throws Exception
  {
    // We have to sleep here because the backup ID that gets generated will be
    // based on a timestamp and we don't want two in the same second.
    Thread.sleep(1100);

    assertEquals(conn.hasPrivilege(Privilege.BACKEND_BACKUP, null),
                 hasPrivilege);

    assertPrivilege(conn, hasPrivilege,
      "dn: ds-task-id=" + UUID.randomUUID() + ",cn=Scheduled Tasks,cn=Tasks",
      "objectclass: top",
      "objectclass: ds-task",
      "objectclass: ds-task-backup",
      "ds-task-class-name: org.opends.server.tasks.BackupTask",
      "ds-backup-directory-path: bak",
      "ds-task-backup-all: TRUE");
  }



  /**
   * Tests to ensure that attempts to restore the Directory Server backends
   * will properly respect the BACKEND_RESTORE privilege.
   *
   * @param  conn          The client connection to use to perform the restore.
   * @param  hasPrivilege  Indicates whether the authenticated user is expected
   *                       to have the BACKEND_RESTORE privilege and therefore
   *                       the restore should succeed.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test(dataProvider = "testdata", groups = { "slow" },
        dependsOnMethods = { "testBackupBackend" })
  public void testRestoreBackend(InternalClientConnection conn,
                                 boolean hasPrivilege)
         throws Exception
  {
    assertEquals(conn.hasPrivilege(Privilege.BACKEND_RESTORE, null),
                 hasPrivilege);

    assertPrivilege(conn, hasPrivilege,
      "dn: ds-task-id=" + UUID.randomUUID() + ",cn=Scheduled Tasks,cn=Tasks",
      "objectclass: top",
      "objectclass: ds-task",
      "objectclass: ds-task-restore",
      "ds-task-class-name: org.opends.server.tasks.RestoreTask",
      "ds-backup-directory-path: bak" + File.separator + "userRoot");
  }



  /**
   * Tests to ensure that attempts to export the contents of a Directory Server
   * backend will properly respect the LDIF_EXPORT privilege.
   *
   * @param  conn          The client connection to use to perform the export.
   * @param  hasPrivilege  Indicates whether the authenticated user is expected
   *                       to have the LDIF_EXPORT privilege and therefore
   *                       the export should succeed.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test(dataProvider = "testdata", groups = { "slow" })
  public void testLDIFExport(InternalClientConnection conn,
                             boolean hasPrivilege)
         throws Exception
  {
    assertEquals(conn.hasPrivilege(Privilege.LDIF_EXPORT, null), hasPrivilege);

    File   tempFile     = File.createTempFile("export-", ".ldif");
    String tempFilePath = tempFile.getAbsolutePath();
    tempFile.delete();

    try
    {
      assertPrivilege(conn, hasPrivilege,
          "dn: ds-task-id=" + UUID.randomUUID() + ",cn=Scheduled Tasks,cn=Tasks",
          "objectclass: top",
          "objectclass: ds-task",
          "objectclass: ds-task-export",
          "ds-task-class-name: org.opends.server.tasks.ExportTask",
          "ds-task-export-backend-id: userRoot",
          "ds-task-export-ldif-file: " + tempFilePath);
    }
    finally
    {
      tempFile.delete();
    }
  }



  /**
   * Tests to ensure that attempts to import into a Directory Server backend
   * will properly respect the LDIF_IMPORT privilege.
   *
   * @param  conn          The client connection to use to perform the import.
   * @param  hasPrivilege  Indicates whether the authenticated user is expected
   *                       to have the LDIF_IMPORT privilege and therefore
   *                       the import should succeed.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test(dataProvider = "testdata", groups = { "slow" })
  public void testLDIFImport(InternalClientConnection conn,
                             boolean hasPrivilege)
         throws Exception
  {
    assertEquals(conn.hasPrivilege(Privilege.LDIF_IMPORT, null), hasPrivilege);

    String path = TestCaseUtils.createTempFile(
      "dn: dc=example,dc=com",
      "objectClass: top",
      "objectClass: domain",
      "dc: example");

    assertPrivilege(conn, hasPrivilege,
      "dn: ds-task-id=" + UUID.randomUUID() + ",cn=Scheduled Tasks,cn=Tasks",
      "objectclass: top",
      "objectclass: ds-task",
      "objectclass: ds-task-import",
      "ds-task-class-name: org.opends.server.tasks.ImportTask",
      "ds-task-import-backend-id: userRoot",
      "ds-task-import-ldif-file: " + path);
  }

  /**
   * Test to ensure that attempts to rebuild indexes will property respect
   * the LDIF_IMPORT privilege.
   *
   * @param conn The client connection to use to perform the rebuild.
   * @param hasPrivilege Indicates weather the authenticated user is
   *                     expected to have the INDEX_REBUILD privilege
   *                     and therefore the rebuild should succeed.
   * @throws Exception if an unexpected problem occurs.
   */
  @Test(dataProvider = "testdata", groups = { "slow" })
  public void testRebuildIndex(InternalClientConnection conn,
                               boolean hasPrivilege)
      throws Exception
  {
    assertEquals(conn.hasPrivilege(Privilege.LDIF_IMPORT, null), hasPrivilege);

    assertPrivilege(conn, hasPrivilege,
      "dn: ds-task-id=" + UUID.randomUUID() + ",cn=Scheduled Tasks,cn=Tasks",
      "objectclass: top",
      "objectclass: ds-task",
      "objectclass: ds-task-rebuild",
      "ds-task-class-name: org.opends.server.tasks.RebuildTask",
      "ds-task-rebuild-base-dn: dc=example,dc=com",
      "ds-task-rebuild-index: cn");
  }



  /**
   * Tests to ensure that the use of the Directory Server will properly respect
   * the PROXIED_AUTH privilege for add, delete, modify and modify DN requests
   * that contain the proxied auth v1 control.
   *
   * @param  conn          The client connection to use to perform the
   *                       operation.
   * @param  hasPrivilege  Indicates whether the authenticated user is expected
   *                       to have the PROXIED_AUTH privilege and therefore
   *                       the operation should succeed.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test(dataProvider = "testdata")
  public void testProxyAuthV1Write(InternalClientConnection conn,
                                   boolean hasPrivilege)
         throws Exception
  {
    // We can't trust the value of hasPrivilege because root users don't get
    // proxy privileges by default.  So make the determination based on the
    // privileges the user actually has.
    boolean hasProxyPrivilege = conn.hasPrivilege(Privilege.PROXIED_AUTH, null);

    Entry e = TestCaseUtils.makeEntry(
      "dn: cn=ProxyV1 Test,o=test",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "cn: ProxyV1 Test",
      "givenName: ProxyV1",
      "sn: Test");

    List<Control> controls = new ArrayList<>(1);
    controls.add(new ProxiedAuthV1Control(DN.valueOf("cn=PWReset Target,o=test")));


    // Try to add the entry.  If this fails with the proxy control, then add it
    // with a root connection so we can do other things with it.
    AddOperation addOperation = new AddOperationBasis(conn, nextOperationID(), nextMessageID(),
                          controls, e.getName(), e.getObjectClasses(),
                          e.getUserAttributes(), e.getOperationalAttributes());
    addOperation.run();
    assertProxyPrivilege(addOperation.getResultCode(), hasProxyPrivilege);

    if (!hasProxyPrivilege)
    {
      TestCaseUtils.addEntry(e);
    }


    // Try to modify the entry to add a description.
    List<Modification> mods = new ArrayList<>(1);
    mods.add(new Modification(ModificationType.REPLACE,
        Attributes.create("description", "foo")));

    ModifyOperation modifyOperation = new ModifyOperationBasis(conn, nextOperationID(), nextMessageID(),
                             controls, e.getName(), mods);
    modifyOperation.run();
    assertProxyPrivilege(modifyOperation.getResultCode(), hasProxyPrivilege);


    // Try to rename the entry.
    ModifyDNOperation modifyDNOperation = new ModifyDNOperationBasis(conn, nextOperationID(),
                               nextMessageID(), controls, e.getName(),
                               RDN.decode("cn=Proxy V1 Test"), true, null);
    modifyDNOperation.run();
    assertProxyPrivilege(modifyOperation.getResultCode(), hasProxyPrivilege);

    DN newEntryDN = e.getName();
    if (hasProxyPrivilege)
    {
      newEntryDN = modifyDNOperation.getNewDN();
    }


    // Try to delete the operation.  If this fails, then delete it with a root
    // connection so it gets cleaned up.
    DeleteOperation deleteOperation = new DeleteOperationBasis(conn, nextOperationID(), nextMessageID(),
                             controls, newEntryDN);
    deleteOperation.run();
    assertProxyPrivilege(deleteOperation.getResultCode(), hasProxyPrivilege);

    if (!hasProxyPrivilege)
    {
      DeleteOperation delOp = getRootConnection().processDelete(newEntryDN);
      assertEquals(delOp.getResultCode(), ResultCode.SUCCESS);
    }
  }



  /**
   * Tests to ensure that the use of the Directory Server will properly respect
   * the PROXIED_AUTH privilege for search and compare requests that contain the
   * proxied auth v1 control.
   *
   * @param  conn          The client connection to use to perform the
   *                       operation.
   * @param  hasPrivilege  Indicates whether the authenticated user is expected
   *                       to have the PROXIED_AUTH privilege and therefore
   *                       the operation should succeed.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test(dataProvider = "testdata")
  public void testProxyAuthV1Read(InternalClientConnection conn,
                                  boolean hasPrivilege)
         throws Exception
  {
    // We can't trust the value of hasPrivilege because root users don't get
    // proxy privileges by default.  So make the determination based on the
    // privileges the user actually has.
    boolean hasProxyPrivilege = conn.hasPrivilege(Privilege.PROXIED_AUTH, null);

    DN targetDN = DN.valueOf("cn=PWReset Target,o=test");
    List<Control> controls = new ArrayList<>(1);
    controls.add(new ProxiedAuthV1Control(targetDN));


    // Test a compare operation against the PWReset Target user.
    CompareOperation compareOperation = new CompareOperationBasis(conn,
                              nextOperationID(), nextMessageID(),
                              controls, targetDN,
                              DirectoryServer.getAttributeTypeOrDefault("cn"),
                              ByteString.valueOfUtf8("PWReset Target"));
    compareOperation.run();
    if (hasProxyPrivilege)
    {
      assertEquals(compareOperation.getResultCode(), COMPARE_TRUE);
    }
    else
    {
      assertEquals(compareOperation.getResultCode(), AUTHORIZATION_DENIED);
    }


    // Test a search operation against the PWReset Target user.
    SearchRequest request = newSearchRequest(targetDN, SearchScope.BASE_OBJECT).addControl(controls);
    InternalSearchOperation searchOperation = conn.processSearch(request);
    assertProxyPrivilege(searchOperation.getResultCode(), hasProxyPrivilege);
  }



  /**
   * Tests to ensure that the use of the Directory Server will properly respect
   * the PROXIED_AUTH privilege for add, delete, modify and modify DN requests
   * that contain the proxied auth v2 control.
   *
   * @param  conn          The client connection to use to perform the
   *                       operation.
   * @param  hasPrivilege  Indicates whether the authenticated user is expected
   *                       to have the PROXIED_AUTH privilege and therefore
   *                       the operation should succeed.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test(dataProvider = "testdata")
  public void testProxyAuthV2Write(InternalClientConnection conn,
                                   boolean hasPrivilege)
         throws Exception
  {
    // We can't trust the value of hasPrivilege because root users don't get
    // proxy privileges by default.  So make the determination based on the
    // privileges the user actually has.
    boolean hasProxyPrivilege = conn.hasPrivilege(Privilege.PROXIED_AUTH, null);

    Entry e = TestCaseUtils.makeEntry(
      "dn: cn=ProxyV2 Test,o=test",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "cn: ProxyV2 Test",
      "givenName: ProxyV2",
      "sn: Test");

    List<Control> controls = new ArrayList<>(1);
    controls.add(new ProxiedAuthV2Control(ByteString.valueOfUtf8("dn:cn=PWReset Target,o=test")));


    // Try to add the entry.  If this fails with the proxy control, then add it
    // with a root connection so we can do other things with it.
    AddOperation addOperation =
         new AddOperationBasis(conn, nextOperationID(), nextMessageID(),
                          controls, e.getName(), e.getObjectClasses(),
                          e.getUserAttributes(), e.getOperationalAttributes());
    addOperation.run();
    assertProxyPrivilege(addOperation.getResultCode(), hasProxyPrivilege);

    if (!hasProxyPrivilege)
    {
      TestCaseUtils.addEntry(e);
    }


    // Try to modify the entry to add a description.
    List<Modification> mods = new ArrayList<>(1);
    mods.add(new Modification(ModificationType.REPLACE,
        Attributes.create("description", "foo")));

    ModifyOperation modifyOperation =
         new ModifyOperationBasis(conn, nextOperationID(), nextMessageID(),
                             controls, e.getName(), mods);
    modifyOperation.run();
    assertProxyPrivilege(modifyOperation.getResultCode(), hasProxyPrivilege);


    // Try to rename the entry.
    ModifyDNOperation modifyDNOperation = new ModifyDNOperationBasis(conn, nextOperationID(),
                               nextMessageID(), controls, e.getName(),
                               RDN.decode("cn=Proxy V2 Test"), true, null);
    modifyDNOperation.run();
    assertProxyPrivilege(modifyDNOperation.getResultCode(), hasProxyPrivilege);

    DN newEntryDN = e.getName();
    if (hasProxyPrivilege)
    {
      newEntryDN = modifyDNOperation.getNewDN();
    }


    // Try to delete the operation.  If this fails, then delete it with a root
    // connection so it gets cleaned up.
    DeleteOperationBasis deleteOperation = new DeleteOperationBasis(conn, nextOperationID(), nextMessageID(),
                             controls, newEntryDN);
    deleteOperation.run();
    assertProxyPrivilege(deleteOperation.getResultCode(), hasProxyPrivilege);

    if (!hasProxyPrivilege)
    {
      DeleteOperation delOp = getRootConnection().processDelete(newEntryDN);
      assertEquals(delOp.getResultCode(), ResultCode.SUCCESS);
    }
  }



  /**
   * Tests to ensure that the use of the Directory Server will properly respect
   * the PROXIED_AUTH privilege for search and compare requests that contain the
   * proxied auth v2 control.
   *
   * @param  conn          The client connection to use to perform the
   *                       operation.
   * @param  hasPrivilege  Indicates whether the authenticated user is expected
   *                       to have the PROXIED_AUTH privilege and therefore
   *                       the operation should succeed.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test(dataProvider = "testdata")
  public void testProxyAuthV2Read(InternalClientConnection conn,
                                  boolean hasPrivilege)
         throws Exception
  {
    // We can't trust the value of hasPrivilege because root users don't get
    // proxy privileges by default.  So make the determination based on the
    // privileges the user actually has.
    boolean hasProxyPrivilege = conn.hasPrivilege(Privilege.PROXIED_AUTH, null);

    DN targetDN = DN.valueOf("cn=PWReset Target,o=test");
    List<Control> controls = new ArrayList<>(1);
    controls.add(new ProxiedAuthV2Control(ByteString.valueOfUtf8("dn:" + targetDN)));


    // Test a compare operation against the PWReset Target user.
    CompareOperation compareOperation = new CompareOperationBasis(conn, nextOperationID(),
                              nextMessageID(), controls, targetDN,
                              DirectoryServer.getAttributeTypeOrDefault("cn"),
             ByteString.valueOfUtf8("PWReset Target"));
    compareOperation.run();
    if (hasProxyPrivilege)
    {
      assertEquals(compareOperation.getResultCode(), COMPARE_TRUE);
    }
    else
    {
      assertEquals(compareOperation.getResultCode(), AUTHORIZATION_DENIED);
    }


    // Test a search operation against the PWReset Target user.
    SearchRequest request = newSearchRequest(targetDN, SearchScope.BASE_OBJECT).addControl(controls);
    InternalSearchOperation searchOperation = conn.processSearch(request);
    searchOperation.run();
    assertProxyPrivilege(searchOperation.getResultCode(), hasProxyPrivilege);
  }



  /**
   * Tests to ensure that the server will properly respect the PROXIED_AUTH
   * privilege when attempting to perform DIGEST-MD5 authentication when an
   * anonymous authorization ID is specified with an authentication identity
   * that has sufficient privileges and using the "dn:" syntax.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testDIGESTMD5AnonymousAuthzIDSuccessfulDNColon()
         throws Exception
  {
    String[] args =
    {
      "--noPropertiesFile",
      "-h", "127.0.0.1",
      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
      "-o", "mech=DIGEST-MD5",
      "-o", "authid=dn:cn=Privileged User,o=test",
      "-o", "authzid=dn:",
      "-w", "password",
      "-b", "o=test",
      "-s", "base",
      "(objectClass=*)"
    };

    assertEquals(runSearchWithSystemErr(args), 0);
  }



  /**
   * Tests to ensure that the server will behave properly when attempting to
   * perform DIGEST-MD5 authentication when an authorization ID equaling the
   * authentication ID is specified with an authentication identity that has
   * sufficient privileges and using the "dn:" syntax.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testDIGESTMD5SameAuthzIDSuccessfulDNColon()
         throws Exception
  {
    String[] args =
    {
      "--noPropertiesFile",
      "-h", "127.0.0.1",
      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
      "-o", "mech=DIGEST-MD5",
      "-o", "authid=dn:cn=Privileged User,o=test",
      "-o", "authzid=dn:cn=Privileged User,o=test",
      "-w", "password",
      "-b", "o=test",
      "-s", "base",
      "(objectClass=*)"
    };

    assertEquals(runSearchWithSystemErr(args), 0);
  }



  /**
   * Tests to ensure that the server will properly respect the PROXIED_AUTH
   * privilege when attempting to perform DIGEST-MD5 authentication when an
   * alternate authorization ID is specified with an authentication identity
   * that has sufficient privileges and using the "dn:" syntax.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testDIGESTMD5DifferentAuthzIDSuccessfulDNColon()
         throws Exception
  {
    String[] args =
    {
      "--noPropertiesFile",
      "-h", "127.0.0.1",
      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
      "-o", "mech=DIGEST-MD5",
      "-o", "authid=dn:cn=Privileged User,o=test",
      "-o", "authzid=dn:cn=Unprivileged User,o=test",
      "-w", "password",
      "-b", "o=test",
      "-s", "base",
      "(objectClass=*)"
    };

    assertEquals(runSearchWithSystemErr(args), 0);
  }



  /**
   * Tests to ensure that the server will properly respect the PROXIED_AUTH
   * privilege when attempting to perform DIGEST-MD5 authentication when an
   * anonymous authorization ID is specified with an authentication identity
   * that has sufficient privileges and using the "u:" syntax.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testDIGESTMD5AnonymousAuthzIDSuccessfulUColon()
         throws Exception
  {
    String[] args =
    {
      "--noPropertiesFile",
      "-h", "127.0.0.1",
      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
      "-o", "mech=DIGEST-MD5",
      "-o", "authid=u:privileged.user",
      "-o", "authzid=u:",
      "-w", "password",
      "-b", "o=test",
      "-s", "base",
      "(objectClass=*)"
    };

    assertEquals(runSearchWithSystemErr(args), 0);
  }



  /**
   * Tests to ensure that the server will behave properly when attempting to
   * perform DIGEST-MD5 authentication when an authorization ID equaling the
   * authentication ID is specified with an authentication identity that has
   * sufficient privileges and using the "u:" syntax.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testDIGESTMD5SameAuthzIDSuccessfulUColon()
         throws Exception
  {
    String[] args =
    {
      "--noPropertiesFile",
      "-h", "127.0.0.1",
      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
      "-o", "mech=DIGEST-MD5",
      "-o", "authid=u:privileged.user",
      "-o", "authzid=u:privileged.user",
      "-w", "password",
      "-b", "o=test",
      "-s", "base",
      "(objectClass=*)"
    };

    assertEquals(runSearchWithSystemErr(args), 0);
  }



  /**
   * Tests to ensure that the server will properly respect the PROXIED_AUTH
   * privilege when attempting to perform DIGEST-MD5 authentication when an
   * alternate authorization ID is specified with an authentication identity
   * that has sufficient privileges and using the "u:" syntax.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testDIGESTMD5DifferentAuthzIDSuccessfulUColon()
         throws Exception
  {
    String[] args =
    {
      "--noPropertiesFile",
      "-h", "127.0.0.1",
      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
      "-o", "mech=DIGEST-MD5",
      "-o", "authid=u:privileged.user",
      "-o", "authzid=u:unprivileged.user",
      "-w", "password",
      "-b", "o=test",
      "-s", "base",
      "(objectClass=*)"
    };

    assertEquals(runSearchWithSystemErr(args), 0);
  }



  /**
   * Tests to ensure that the server will properly respect the PROXIED_AUTH
   * privilege when attempting to perform DIGEST-MD5 authentication when an
   * anonymous authorization ID is specified with an authentication identity
   * that does not have sufficient privileges and using the "dn:" syntax.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testDIGESTMD5AnonymousAuthzIDFailedDNColon()
         throws Exception
  {
    String[] args =
    {
      "--noPropertiesFile",
      "-h", "127.0.0.1",
      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
      "-o", "mech=DIGEST-MD5",
      "-o", "authid=dn:cn=Unprivileged User,o=test",
      "-o", "authzid=dn:",
      "-w", "password",
      "-b", "o=test",
      "-s", "base",
      "(objectClass=*)"
    };

    Assert.assertNotEquals(runSearch(args), 0);
  }



  /**
   * Tests to ensure that the server will behave properly when attempting to
   * perform DIGEST-MD5 authentication when an authorization ID equaling the
   * authentication ID is specified with an authentication identity that does
   * not have sufficient privileges and using the "dn:" syntax.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testDIGESTMD5SameUnprivAuthzIDSuccessfulDNColon()
         throws Exception
  {
    String[] args =
    {
      "--noPropertiesFile",
      "-h", "127.0.0.1",
      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
      "-o", "mech=DIGEST-MD5",
      "-o", "authid=dn:cn=Unprivileged User,o=test",
      "-o", "authzid=dn:cn=Unprivileged User,o=test",
      "-w", "password",
      "-b", "o=test",
      "-s", "base",
      "(objectClass=*)"
    };

    assertEquals(runSearchWithSystemErr(args), 0);
  }



  /**
   * Tests to ensure that the server will properly respect the PROXIED_AUTH
   * privilege when attempting to perform DIGEST-MD5 authentication when an
   * alternate authorization ID is specified with an authentication identity
   * that does not have sufficient privileges and using the "dn:" syntax.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testDIGESTMD5DifferentAuthzIDFailedDNColon()
         throws Exception
  {
    String[] args =
    {
      "--noPropertiesFile",
      "-h", "127.0.0.1",
      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
      "-o", "mech=DIGEST-MD5",
      "-o", "authid=dn:cn=Unprivileged User,o=test",
      "-o", "authzid=dn:cn=Privileged User,o=test",
      "-w", "password",
      "-b", "o=test",
      "-s", "base",
      "(objectClass=*)"
    };

    Assert.assertNotEquals(runSearch(args), 0);
  }



  /**
   * Tests to ensure that the server will properly respect the PROXIED_AUTH
   * privilege when attempting to perform DIGEST-MD5 authentication when an
   * anonymous authorization ID is specified with an authentication identity
   * that does not have sufficient privileges and using the "u:" syntax.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testDIGESTMD5AnonymousAuthzIDFailedUColon()
         throws Exception
  {
    String[] args =
    {
      "--noPropertiesFile",
      "-h", "127.0.0.1",
      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
      "-o", "mech=DIGEST-MD5",
      "-o", "authid=u:unprivileged.user",
      "-o", "authzid=u:",
      "-w", "password",
      "-b", "o=test",
      "-s", "base",
      "(objectClass=*)"
    };

    Assert.assertNotEquals(runSearch(args), 0);
  }



  /**
   * Tests to ensure that the server will behave properly when attempting to
   * perform DIGEST-MD5 authentication when an authorization ID equaling the
   * authentication ID is specified with an authentication identity that does
   * not have sufficient privileges and using the "u:" syntax.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testDIGESTMD5SameUnprivAuthzIDSuccessfulUColon()
         throws Exception
  {
    String[] args =
    {
      "--noPropertiesFile",
      "-h", "127.0.0.1",
      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
      "-o", "mech=DIGEST-MD5",
      "-o", "authid=u:unprivileged.user",
      "-o", "authzid=u:unprivileged.user",
      "-w", "password",
      "-b", "o=test",
      "-s", "base",
      "(objectClass=*)"
    };

    assertEquals(runSearchWithSystemErr(args), 0);
  }



  /**
   * Tests to ensure that the server will properly respect the PROXIED_AUTH
   * privilege when attempting to perform DIGEST-MD5 authentication when an
   * alternate authorization ID is specified with an authentication identity
   * that does not have sufficient privileges and using the "u:" syntax.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testDIGESTMD5DifferentAuthzIDFailedUColon()
         throws Exception
  {
    String[] args =
    {
      "--noPropertiesFile",
      "-h", "127.0.0.1",
      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
      "-o", "mech=DIGEST-MD5",
      "-o", "authid=u:unprivileged.user",
      "-o", "authzid=u:privileged.user",
      "-w", "password",
      "-b", "o=test",
      "-s", "base",
      "(objectClass=*)"
    };

    Assert.assertNotEquals(runSearch(args), 0);
  }



  /**
   * Tests to ensure that the server will properly respect the PROXIED_AUTH
   * privilege when attempting to perform PLAIN authentication when an
   * anonymous authorization ID is specified with an authentication identity
   * that has sufficient privileges and using the "dn:" syntax.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testPLAINAnonymousAuthzIDSuccessfulDNColon()
         throws Exception
  {
    String[] args =
    {
      "--noPropertiesFile",
      "-h", "127.0.0.1",
      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
      "-o", "mech=PLAIN",
      "-o", "authid=dn:cn=Privileged User,o=test",
      "-o", "authzid=dn:",
      "-w", "password",
      "-b", "o=test",
      "-s", "base",
      "(objectClass=*)"
    };

    assertEquals(runSearchWithSystemErr(args), 0);
  }



  /**
   * Tests to ensure that the server will behave properly when attempting to
   * perform PLAIN authentication when an authorization ID equaling the
   * authentication ID is specified with an authentication identity that has
   * sufficient privileges and using the "dn:" syntax.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testPLAINSameAuthzIDSuccessfulDNColon()
         throws Exception
  {
    String[] args =
    {
      "--noPropertiesFile",
      "-h", "127.0.0.1",
      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
      "-o", "mech=PLAIN",
      "-o", "authid=dn:cn=Privileged User,o=test",
      "-o", "authzid=dn:cn=Privileged User,o=test",
      "-w", "password",
      "-b", "o=test",
      "-s", "base",
      "(objectClass=*)"
    };

    assertEquals(runSearchWithSystemErr(args), 0);
  }



  /**
   * Tests to ensure that the server will properly respect the PROXIED_AUTH
   * privilege when attempting to perform PLAIN authentication when an
   * alternate authorization ID is specified with an authentication identity
   * that has sufficient privileges and using the "dn:" syntax.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testPLAINDifferentAuthzIDSuccessfulDNColon()
         throws Exception
  {
    String[] args =
    {
      "--noPropertiesFile",
      "-h", "127.0.0.1",
      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
      "-o", "mech=PLAIN",
      "-o", "authid=dn:cn=Privileged User,o=test",
      "-o", "authzid=dn:cn=Unprivileged User,o=test",
      "-w", "password",
      "-b", "o=test",
      "-s", "base",
      "(objectClass=*)"
    };

    assertEquals(runSearchWithSystemErr(args), 0);
  }



  /**
   * Tests to ensure that the server will properly respect the PROXIED_AUTH
   * privilege when attempting to perform PLAIN authentication when an
   * anonymous authorization ID is specified with an authentication identity
   * that has sufficient privileges and using the "u:" syntax.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testPLAINAnonymousAuthzIDSuccessfulUColon()
         throws Exception
  {
    String[] args =
    {
      "--noPropertiesFile",
      "-h", "127.0.0.1",
      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
      "-o", "mech=PLAIN",
      "-o", "authid=u:privileged.user",
      "-o", "authzid=u:",
      "-w", "password",
      "-b", "o=test",
      "-s", "base",
      "(objectClass=*)"
    };

    assertEquals(runSearchWithSystemErr(args), 0);
  }



  /**
   * Tests to ensure that the server will behave properly when attempting to
   * perform PLAIN authentication when an authorization ID equaling the
   * authentication ID is specified with an authentication identity that has
   * sufficient privileges and using the "u:" syntax.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testPLAINSameAuthzIDSuccessfulUColon()
         throws Exception
  {
    String[] args =
    {
      "--noPropertiesFile",
      "-h", "127.0.0.1",
      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
      "-o", "mech=PLAIN",
      "-o", "authid=u:privileged.user",
      "-o", "authzid=u:privileged.user",
      "-w", "password",
      "-b", "o=test",
      "-s", "base",
      "(objectClass=*)"
    };

    assertEquals(runSearchWithSystemErr(args), 0);
  }



  /**
   * Tests to ensure that the server will properly respect the PROXIED_AUTH
   * privilege when attempting to perform PLAIN authentication when an
   * alternate authorization ID is specified with an authentication identity
   * that has sufficient privileges and using the "u:" syntax.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testPLAINDifferentAuthzIDSuccessfulUColon()
         throws Exception
  {
    String[] args =
    {
      "--noPropertiesFile",
      "-h", "127.0.0.1",
      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
      "-o", "mech=PLAIN",
      "-o", "authid=u:privileged.user",
      "-o", "authzid=u:unprivileged.user",
      "-w", "password",
      "-b", "o=test",
      "-s", "base",
      "(objectClass=*)"
    };

    assertEquals(runSearchWithSystemErr(args), 0);
  }



  /**
   * Tests to ensure that the server will properly respect the PROXIED_AUTH
   * privilege when attempting to perform PLAIN authentication when an
   * anonymous authorization ID is specified with an authentication identity
   * that does not have sufficient privileges and using the "dn:" syntax.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testPLAINAnonymousAuthzIDFailedDNColon()
         throws Exception
  {
    String[] args =
    {
      "--noPropertiesFile",
      "-h", "127.0.0.1",
      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
      "-o", "mech=PLAIN",
      "-o", "authid=dn:cn=Unprivileged User,o=test",
      "-o", "authzid=dn:",
      "-w", "password",
      "-b", "o=test",
      "-s", "base",
      "(objectClass=*)"
    };

    Assert.assertNotEquals(runSearch(args), 0);
  }



  /**
   * Tests to ensure that the server will behave properly when attempting to
   * perform PLAIN authentication when an authorization ID equaling the
   * authentication ID is specified with an authentication identity that does
   * not have sufficient privileges and using the "dn:" syntax.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testPLAINSameUnprivAuthzIDSuccessfulDNColon()
         throws Exception
  {
    String[] args =
    {
      "--noPropertiesFile",
      "-h", "127.0.0.1",
      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
      "-o", "mech=PLAIN",
      "-o", "authid=dn:cn=Unprivileged User,o=test",
      "-o", "authzid=dn:cn=Unprivileged User,o=test",
      "-w", "password",
      "-b", "o=test",
      "-s", "base",
      "(objectClass=*)"
    };

    assertEquals(runSearchWithSystemErr(args), 0);
  }



  /**
   * Tests to ensure that the server will properly respect the PROXIED_AUTH
   * privilege when attempting to perform PLAIN authentication when an
   * alternate authorization ID is specified with an authentication identity
   * that does not have sufficient privileges and using the "dn:" syntax.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testPLAINDifferentAuthzIDFailedDNColon()
         throws Exception
  {
    String[] args =
    {
      "--noPropertiesFile",
      "-h", "127.0.0.1",
      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
      "-o", "mech=PLAIN",
      "-o", "authid=dn:cn=Unprivileged User,o=test",
      "-o", "authzid=dn:cn=Privileged User,o=test",
      "-w", "password",
      "-b", "o=test",
      "-s", "base",
      "(objectClass=*)"
    };

    Assert.assertNotEquals(runSearch(args), 0);
  }



  /**
   * Tests to ensure that the server will properly respect the PROXIED_AUTH
   * privilege when attempting to perform PLAIN authentication when an
   * anonymous authorization ID is specified with an authentication identity
   * that does not have sufficient privileges and using the "u:" syntax.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testPLAINAnonymousAuthzIDFailedUColon()
         throws Exception
  {
    String[] args =
    {
      "--noPropertiesFile",
      "-h", "127.0.0.1",
      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
      "-o", "mech=PLAIN",
      "-o", "authid=u:unprivileged.user",
      "-o", "authzid=u:",
      "-w", "password",
      "-b", "o=test",
      "-s", "base",
      "(objectClass=*)"
    };

    Assert.assertNotEquals(runSearch(args), 0);
  }



  /**
   * Tests to ensure that the server will behave properly when attempting to
   * perform PLAIN authentication when an authorization ID equaling the
   * authentication ID is specified with an authentication identity that does
   * not have sufficient privileges and using the "u:" syntax.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testPLAINSameUnprivAuthzIDSuccessfulUColon()
         throws Exception
  {
    String[] args =
    {
      "--noPropertiesFile",
      "-h", "127.0.0.1",
      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
      "-o", "mech=PLAIN",
      "-o", "authid=u:unprivileged.user",
      "-o", "authzid=u:unprivileged.user",
      "-w", "password",
      "-b", "o=test",
      "-s", "base",
      "(objectClass=*)"
    };

    assertEquals(runSearchWithSystemErr(args), 0);
  }



  /**
   * Tests to ensure that the server will properly respect the PROXIED_AUTH
   * privilege when attempting to perform PLAIN authentication when an
   * alternate authorization ID is specified with an authentication identity
   * that does not have sufficient privileges and using the "u:" syntax.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testPLAINDifferentAuthzIDFailedUColon()
         throws Exception
  {
    String[] args =
    {
      "--noPropertiesFile",
      "-h", "127.0.0.1",
      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
      "-o", "mech=PLAIN",
      "-o", "authid=u:unprivileged.user",
      "-o", "authzid=u:privileged.user",
      "-w", "password",
      "-b", "o=test",
      "-s", "base",
      "(objectClass=*)"
    };

    Assert.assertNotEquals(runSearch(args), 0);
  }



  /**
   * Tests the ability to disable a privilege so that an operation which will
   * fail for a user without an appropriate privilege will succeed if that
   * privilege is disabled.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testDisablePrivilege()
         throws Exception
  {
    // Make sure that the operation fails when the privilege is not disabled.
    String[] searchArgs =
    {
      "--noPropertiesFile",
      "-h", "127.0.0.1",
      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
      "-o", "mech=PLAIN",
      "-o", "authid=dn:cn=Directory Manager",
      "-o", "authzid=u:privileged.user",
      "-w", "password",
      "-b", "o=test",
      "-s", "base",
      "(objectClass=*)"
    };

    Assert.assertNotEquals(runSearch(searchArgs), 0);


    // Disable the PROXIED_AUTH privilege and verify that the operation now
    // succeeds.
    TestCaseUtils.dsconfig(
      "set-global-configuration-prop",
      "--add", "disabled-privilege:proxied-auth");
    assertEquals(runSearch(searchArgs), 0);


    // Re-enable the PROXIED_AUTH privilege and verify that the operation
    // fails again.
    TestCaseUtils.dsconfig(
      "set-global-configuration-prop",
      "--remove", "disabled-privilege:proxied-auth");

    Assert.assertNotEquals(runSearch(searchArgs), 0);
  }

  private int runSearch(String[] args)
  {
    return LDAPSearch.mainSearch(args, false, null, null);
  }

  private int runSearchWithSystemErr(String[] args)
  {
    return LDAPSearch.mainSearch(args, false, null, System.err);
  }

  /**
   * Tests the ability to update the set of privileges for a user on the fly
   * and have them take effect immediately.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testUpdateUserPrivileges()
         throws Exception
  {
    InternalClientConnection rootConnection =
         InternalClientConnection.getRootConnection();

    TestCaseUtils.addEntry(
      "dn: cn=Test User,o=test",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "cn: Test User",
      "givenName: Test",
      "sn: User",
      "userPassword: password");

    // We won't use an internal connection here because these are not notified
    // of dynamic changes to authentication info.
    Socket s = new Socket("127.0.0.1", TestCaseUtils.getServerLdapPort());
    try
    {
      TestCaseUtils.configureSocket(s);
      LDAPReader r = new LDAPReader(s);
      LDAPWriter w = new LDAPWriter(s);

      BindRequestProtocolOp bindRequest = new BindRequestProtocolOp(
          ByteString.valueOfUtf8("cn=Test User,o=test"), 3,
          ByteString.valueOfUtf8("password"));
      LDAPMessage message = new LDAPMessage(1, bindRequest);
      w.writeMessage(message);

      message = r.readMessage();
      BindResponseProtocolOp bindResponse = message.getBindResponseProtocolOp();
      assertEquals(bindResponse.getResultCode(), 0);

      CopyOnWriteArraySet<ClientConnection> connections = DirectoryServer
          .getAuthenticatedUsers().get(DN.valueOf("cn=Test User,o=test"));

      assertNotNull(connections);
      assertEquals(connections.size(), 1);
      ClientConnection testConnection = connections.iterator().next();

      // Make sure the user starts out without any privileges.
      for (Privilege p : Privilege.values())
      {
        assertFalse(testConnection.hasPrivilege(p, null));
      }

      // Modify the user entry to add the CONFIG_READ privilege and verify that
      // the client connection reflects that.
      List<Modification> mods = new ArrayList<>();
      mods.add(new Modification(ModificationType.ADD, Attributes.create(
          "ds-privilege-name", "config-read")));
      ModifyOperation modifyOperation = rootConnection.processModify(
          DN.valueOf("cn=Test User,o=test"), mods);
      assertEquals(modifyOperation.getResultCode(), ResultCode.SUCCESS);
      assertTrue(testConnection.hasPrivilege(Privilege.CONFIG_READ, null));

      // Take the privilege away from the user and verify that it is recognized
      // immediately.
      mods.clear();
      mods.add(new Modification(ModificationType.DELETE, Attributes.create(
          "ds-privilege-name", "config-read")));
      modifyOperation = rootConnection.processModify(
          DN.valueOf("cn=Test User,o=test"), mods);
      assertEquals(modifyOperation.getResultCode(), ResultCode.SUCCESS);
      assertFalse(testConnection.hasPrivilege(Privilege.CONFIG_READ, null));

      DeleteOperation deleteOperation = rootConnection.processDelete("cn=Test User,o=test");
      assertEquals(deleteOperation.getResultCode(), ResultCode.SUCCESS);
    }
    finally
    {
      s.close();
    }
  }



  /**
   * Tests the ability to update the set of root privileges and have them take
   * effect immediately for new root connections.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testUpdateRootPrivileges() throws Exception
  {
    // Make sure that a root connection doesn't  have the proxied auth
    // privilege.
    DN unprivRootDN = DN.valueOf("cn=Unprivileged Root,cn=Root DNs,cn=config");
    Entry unprivRootEntry = DirectoryServer.getEntry(unprivRootDN);
    AuthenticationInfo authInfo = new AuthenticationInfo(unprivRootEntry, true);
    InternalClientConnection unprivRootConn =
         new InternalClientConnection(authInfo);
    assertFalse(unprivRootConn.hasPrivilege(Privilege.PROXIED_AUTH, null));


    // Update the set of root privileges to include proxied auth.
    InternalClientConnection internalRootConn = getRootConnection();

    List<Modification> mods = new ArrayList<>();
    mods.add(new Modification(ModificationType.ADD,
        Attributes.create("ds-cfg-default-root-privilege-name",
                                    "proxied-auth")));
    ModifyOperation modifyOperation = internalRootConn.processModify(DN.valueOf("cn=Root DNs,cn=config"), mods);
    assertEquals(modifyOperation.getResultCode(), ResultCode.SUCCESS);


    // Get a new root connection and verify that it now has proxied auth.
    unprivRootEntry = DirectoryServer.getEntry(unprivRootDN);
    authInfo = new AuthenticationInfo(unprivRootEntry, true);
    unprivRootConn = new InternalClientConnection(authInfo);
    assertTrue(unprivRootConn.hasPrivilege(Privilege.PROXIED_AUTH, null));


    // Update the set of root privileges to revoke proxied auth.
    mods.clear();
    mods.add(new Modification(ModificationType.DELETE,
        Attributes.create("ds-cfg-default-root-privilege-name",
                                    "proxied-auth")));
    modifyOperation = internalRootConn.processModify(DN.valueOf("cn=Root DNs,cn=config"), mods);
    assertEquals(modifyOperation.getResultCode(), ResultCode.SUCCESS);


    // Get a new root connection and verify that it no longer has proxied auth.
    unprivRootEntry = DirectoryServer.getEntry(unprivRootDN);
    authInfo = new AuthenticationInfo(unprivRootEntry, true);
    unprivRootConn = new InternalClientConnection(authInfo);
    assertFalse(unprivRootConn.hasPrivilege(Privilege.PROXIED_AUTH, null));
  }



  /**
   * Tests to ensure that the set of privileges defined in the server matches
   * the set of privileges that can be configured as default root privileges,
   * and that it also matches the set of privileges that can be disabled.
   */
  @Test
  public void testConfigurablePrivilegeSets()
  {
    Set<String> serverPrivNames = new HashSet<>();
    for (Privilege p : Privilege.values())
    {
      serverPrivNames.add(p.toString());
    }

    Set<String> defaultRootPrivNames = new HashSet<>();
    for (RootDNCfgDefn.DefaultRootPrivilegeName p :
         RootDNCfgDefn.DefaultRootPrivilegeName.values())
    {
      defaultRootPrivNames.add(p.toString());
      assertTrue(serverPrivNames.contains(p.toString()),
                 "The set of server privileges does not contain potential " +
                 "default root privilege " + p);
    }

    Set<String> disableablePrivNames = new HashSet<>();
    for (DisabledPrivilege p : DisabledPrivilege.values())
    {
      disableablePrivNames.add(p.toString());
      assertTrue(serverPrivNames.contains(p.toString()),
                 "The set of server privileges does not contain disableable " +
                 "privilege " + p);
    }

    for (String s : serverPrivNames)
    {
      assertTrue(defaultRootPrivNames.contains(s),
                 "The set of available default root privileges does not " +
                 "contain server privilege " + s);

      assertTrue(disableablePrivNames.contains(s),
                 "The set of disableable privileges does not contain server " +
                 "privilege " + s);
    }
  }



  /**
   * 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.valueOf("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);
      }
    }

    assertNotNull(task, "There is no such task " + taskEntryDN);

    if (! TaskState.isDone(task.getTaskState()))
    {
      long stopWaitingTime = System.currentTimeMillis() + 20000L;
      while (!TaskState.isDone(task.getTaskState())
          && System.currentTimeMillis() < stopWaitingTime)
      {
        Thread.sleep(10);
      }
    }
    assertTrue(TaskState.isDone(task.getTaskState()),
        "Task " + taskEntryDN + " did not complete in a timely manner.");

    return task;
  }
}
