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

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.util.ArrayList;
import java.util.HashMap;

import org.forgerock.i18n.LocalizableMessage;
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.controls.ProxiedAuthV1Control;
import org.opends.server.controls.ProxiedAuthV2Control;
import org.opends.server.core.AddOperationBasis;
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.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.InternalSearchOperation;
import org.opends.server.protocols.internal.SearchRequest;
import org.opends.server.types.*;
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.ModificationType.*;
import static org.opends.messages.ProtocolMessages.*;
import static org.opends.server.protocols.internal.InternalClientConnection.*;
import static org.opends.server.protocols.internal.Requests.*;
import static org.opends.server.util.CollectionUtils.*;
import static org.testng.Assert.*;

/** This class provides a set of test cases for the Directory Server JMX privilege subsystem. */
public class JmxPrivilegeTestCase extends JmxTestCase
{
  /**
   * An array of boolean values that indicates whether config read operations
   * should be successful for users in the corresponding slots of the connections array.
   */
  private boolean[] successful;

  /** The set of client connections that should be used when performing operations. */
  private JmxClientConnection[] connections;

  /**
   * 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(alwaysRun = true)
  @Override
  public void setUp() throws Exception
  {
    super.setUp();

    TestCaseUtils.enableBackend("unindexedRoot");
    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: -jmx-read",
      "ds-privilege-name: -jmx-write",
      "",
      "dn: cn=Unprivileged JMX 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",
      "",
      "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",
      "ds-privilege-name: jmx-read",
      "ds-privilege-name: jmx-write",
      "",
      "",
      "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: jmx-read",
      "ds-privilege-name: jmx-write",
      "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=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.
    ArrayList<JmxClientConnection> connList = new ArrayList<>();
    ArrayList<Boolean> successList = new ArrayList<>();
    JmxConnectionHandler jmxCtx = getJmxConnectionHandler();

    connList.add(new JmxClientConnection(jmxCtx,new AuthenticationInfo()));
    successList.add(false);

    connList.add(newJmxClientConnection(jmxCtx, "cn=Unprivileged Root,cn=Root DNs,cn=config", true));
    successList.add(false);

    connList.add(newJmxClientConnection(jmxCtx, "cn=Proxy Root,cn=Root DNs,cn=config", true));
    successList.add(true);

    connList.add(newJmxClientConnection(jmxCtx, "cn=Unprivileged User,o=test", false));
    successList.add(false);

    connList.add(newJmxClientConnection(jmxCtx, "cn=Privileged User,o=test", false));
    successList.add(true);


    connections = new JmxClientConnection[connList.size()];
    successful  = new boolean[connections.length];
    for (int i=0; i < connections.length; i++)
    {
      connections[i] = connList.get(i);
      successful[i]  = successList.get(i);
    }

    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",
        "",
        "dn: cn=test2 user,dc=unindexed,dc=jeb",
        "objectClass: top",
        "objectClass: person",
        "objectClass: organizationalPerson",
        "objectClass: inetOrgPerson",
        "cn: test2 user",
        "givenName: user",
        "sn: test2"
    );
  }

  private JmxClientConnection newJmxClientConnection(JmxConnectionHandler jmxCtx, String userDN, boolean isRoot)
      throws DirectoryException
  {
    Entry userEntry = DirectoryServer.getEntry(DN.valueOf(userDN));
    AuthenticationInfo authInfo = new AuthenticationInfo(userEntry, isRoot);
    return new JmxClientConnection(jmxCtx, 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
  {
    processDelete(
        "cn=Unprivileged Root,cn=Root DNs,cn=config",
        "cn=Unprivileged JMX Root,cn=Root DNs,cn=config",
        "cn=Proxy Root,cn=Root DNs,cn=config",
        "cn=Privileged User,o=test",
        "cn=UnPrivileged User,o=test",
        "cn=PWReset Target,o=test",
        "cn=test1 user,dc=unindexed,dc=jeb",
        "cn=test2 user,dc=unindexed,dc=jeb",
        "dc=unindexed,dc=jeb");

    for (int i = 0; connections != null && i < connections.length; i++)
    {
      connections[i].finalize();
      connections[i] = null;
    }
    TestCaseUtils.disableBackend("unindexedRoot");
  }

  private void processDelete(String... userDNs) throws DirectoryException
  {
    for (String userDN : userDNs)
    {
      processDelete(DN.valueOf(userDN));
    }
  }

  /**
   * 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.length][2];
    for (int i=0; i < connections.length; i++)
    {
      returnArray[i][0] = connections[i];
      returnArray[i][1] = successful[i];
    }

    return returnArray;
  }

  /**
   * Check that simple connection to the JMX service are
   * accepted only if JMX_READ privilege is set.
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test(enabled = true)
  public void simpleConnectJmxPrivilege() throws Exception
  {
    OpendsJmxConnector opendsConnector;
    int jmxPort = TestCaseUtils.getServerJmxPort() ;
    HashMap<String, Object> env = new HashMap<>();
    final String user = "cn=Unprivileged JMX Root,cn=Root DNs,cn=config";
    String password  = "password";
    String[] credentials = new String[] { user, password };
    env.put("jmx.remote.credentials", credentials);
    env.put("jmx.remote.x.client.connection.check.period", 0);

    // Try connection withoutJMX_READ privilege
    // Expected result: failed
    try
    {
      opendsConnector = new OpendsJmxConnector("localhost", jmxPort, env);
      opendsConnector.connect();
      opendsConnector.close() ;
      fail("User \"cn=Unprivileged JMX Root,cn=Root "+
          "DNs,cn=config\" doesn't have JMX_READ privilege but he's able " +
          "to connect, which is not the correct behavior");
    }
    catch (SecurityException e)
    {
      LocalizableMessage message = ERR_JMX_INSUFFICIENT_PRIVILEGES.get();
      assertEquals(message.toString(), e.getMessage());
    }

    final DN userDN = DN.valueOf(user);

    // Add JMX_READ privilege
    processModify(userDN, ADD, "ds-privilege-name", "jmx-read");

    //  Try connection withoutJMX_READ privilege
    // Expected result: success
    try
    {
      opendsConnector = new OpendsJmxConnector("localhost", jmxPort, env);
      opendsConnector.connect();
      opendsConnector.close() ;
    }
    catch (SecurityException e)
    {
      fail("User \"cn=Unprivileged JMX Root,cn=Root " +
          "DNs,cn=config\" has JMX_READ privilege and he's NOT able " +
          "to connect, which is NOT the correct behavior.");
    }

    // remove JMX_READ privilege
    processModify(userDN, DELETE, "ds-privilege-name", "jmx-read");

    // Try connection withoutJMX_READ privilege
    // Expected result: failed
    try
    {
      opendsConnector = new OpendsJmxConnector("localhost", jmxPort, env);
      opendsConnector.connect();
      opendsConnector.close() ;
      fail("User \"cn=Unprivileged JMX Root,cn=Root "+
          "DNs,cn=config\" doesn't have JMX_READ privilege but he's able " +
          "to connect, which is not the correct behavior");
    }
    catch (SecurityException e)
    {
      LocalizableMessage message = ERR_JMX_INSUFFICIENT_PRIVILEGES.get();
      assertEquals(message.toString(), e.getMessage());
    }
  }

  /**
   * Tests to ensure that search operations in the server configuration properly
   * respect the JMX_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 JMX_READ privilege and therefore the
   *                       search should succeed.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test(dataProvider = "testdata")
  public void testConfigReadSearch(JmxClientConnection conn, boolean hasPrivilege) throws Exception
  {
    assertEquals(conn.hasPrivilege(Privilege.JMX_READ, null), hasPrivilege);

    SearchRequest request = newSearchRequest(DN.valueOf("cn=config"), SearchScope.BASE_OBJECT);
    InternalSearchOperation searchOp = conn.processSearch(request);
    if (hasPrivilege)
    {
      assertEquals(searchOp.getResultCode(), ResultCode.SUCCESS);
    }
    else
    {
      assertEquals(searchOp.getResultCode(), ResultCode.INSUFFICIENT_ACCESS_RIGHTS);
    }
  }

  /**
   * 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(JmxClientConnection conn, boolean hasPrivilege) throws Exception
  {
    assertEquals(conn.hasPrivilege(Privilege.JMX_WRITE, null), hasPrivilege);

    String schemaDirectory = SchemaConfigManager.getSchemaDirectoryPath();

    String identifier;
    Entry authNEntry = conn.getAuthenticationInfo().getAuthenticationEntry();
    if (authNEntry == null)
    {
      identifier = "null";
    }
    else
    {
      identifier = authNEntry.getName().toString()
          .replace(',', '-')
          .replace(' ', '-')
          .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");
    try (BufferedWriter writer = new BufferedWriter(new FileWriter(validFile)))
    {
      for (String line : fileLines)
      {
        writer.write(line);
        writer.newLine();
      }
    }
  }

  /**
   * 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(JmxClientConnection 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");

    ArrayList<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.
    AddOperationBasis addOp = new AddOperationBasis(
        conn, conn.nextOperationID(), conn.nextMessageID(), controls,
        e.getName(), e.getObjectClasses(), e.getUserAttributes(), e.getOperationalAttributes());
    assertSuccess(hasProxyPrivilege, addOp);
    if (!hasProxyPrivilege)
    {
      TestCaseUtils.addEntry(e);
    }

    // Try to modify the entry to add a description.
    ArrayList<Modification> mods = newModifications(REPLACE, "description", "foo");

    ModifyOperationBasis modifyOp = new ModifyOperationBasis(conn,
        conn.nextOperationID(), conn.nextMessageID(), controls, e.getName(),
        mods);
    assertSuccess(hasProxyPrivilege, modifyOp);

    // Try to rename the entry.
    ModifyDNOperationBasis modifyDNOp = new ModifyDNOperationBasis(
        conn, conn.nextOperationID(), conn.nextMessageID(), controls,
        e.getName(), RDN.decode("cn=Proxy V1 Test"), true, null);
    assertSuccess(hasProxyPrivilege, modifyDNOp);
    DN newEntryDN = hasProxyPrivilege ? modifyDNOp.getNewDN() : e.getName();

    // Try to delete the operation.  If this fails, then delete it with a root
    // connection so it gets cleaned up.
    DeleteOperationBasis deleteOp = new DeleteOperationBasis(conn,
        conn.nextOperationID(), conn.nextMessageID(), controls, newEntryDN);
    assertSuccess(hasProxyPrivilege, deleteOp);
    if (!hasProxyPrivilege)
    {
      processDelete(newEntryDN);
    }
  }

  /**
   * 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(JmxClientConnection 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");
    ArrayList<Control> controls = new ArrayList<>(1);
    controls.add(new ProxiedAuthV1Control(targetDN));

    // Test a compare operation against the PWReset Target user.
    CompareOperationBasis compareOp = new CompareOperationBasis(
        conn, conn.nextOperationID(), conn.nextMessageID(), controls,
        targetDN, DirectoryServer.getAttributeTypeOrDefault("cn"),
        ByteString.valueOfUtf8("PWReset Target"));
    assertSuccess(hasProxyPrivilege, compareOp);

    // Test a search operation against the PWReset Target user.
    SearchRequest request = newSearchRequest(targetDN, SearchScope.BASE_OBJECT).addControl(controls);
    InternalSearchOperation searchOp = new InternalSearchOperation(
        conn, conn.nextOperationID(), conn.nextMessageID(), request, null);
    assertSuccess(hasProxyPrivilege, searchOp);
  }

  /**
   * 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(JmxClientConnection 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");

    ArrayList<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.
    DN authDN = conn.getAuthenticationInfo().getAuthenticationDN();
    AddOperationBasis addOp = new AddOperationBasis(
        conn, conn.nextOperationID(), conn.nextMessageID(), controls,
        e.getName(), e.getObjectClasses(), e.getUserAttributes(), e.getOperationalAttributes());
    assertSuccess(hasProxyPrivilege, authDN, addOp);
    if (!hasProxyPrivilege)
    {
      TestCaseUtils.addEntry(e);
    }

    // Try to modify the entry to add a description.
    ArrayList<Modification> mods = newModifications(REPLACE, "description", "foo");

    ModifyOperationBasis modifyOp = new ModifyOperationBasis(conn,
        conn.nextOperationID(), conn.nextMessageID(), controls, e.getName(),
        mods);
    assertSuccess(hasProxyPrivilege, authDN, modifyOp);

    // Try to rename the entry.
    ModifyDNOperationBasis modifyDNOp = new ModifyDNOperationBasis(
        conn, conn.nextOperationID(), conn.nextMessageID(), controls,
        e.getName(), RDN.decode("cn=Proxy V2 Test"), true, null);
    assertSuccess(hasProxyPrivilege, authDN, modifyDNOp);

    DN newEntryDN = hasProxyPrivilege ? modifyDNOp.getNewDN() : e.getName();

    // Try to delete the operation.  If this fails, then delete it with a root
    // connection so it gets cleaned up.
    DeleteOperationBasis deleteOp = new DeleteOperationBasis(conn,
        conn.nextOperationID(), conn.nextMessageID(), controls, newEntryDN);
    assertSuccess(hasProxyPrivilege, authDN, deleteOp);
    if (!hasProxyPrivilege)
    {
      processDelete(newEntryDN);
    }
  }

  /**
   * 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(JmxClientConnection 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");
    ArrayList<Control> controls = new ArrayList<>(1);
    controls.add(new ProxiedAuthV2Control(ByteString.valueOfUtf8("dn:" + targetDN)));

    // Test a compare operation against the PWReset Target user.
    CompareOperationBasis compareOp = new CompareOperationBasis(
        conn, conn.nextOperationID(), conn.nextMessageID(), controls,
        targetDN, DirectoryServer.getAttributeTypeOrDefault("cn"),
        ByteString.valueOfUtf8("PWReset Target"));
    assertSuccess(hasProxyPrivilege, compareOp);

    // Test a search operation against the PWReset Target user.
    SearchRequest request = newSearchRequest(targetDN, SearchScope.BASE_OBJECT).addControl(controls);
    InternalSearchOperation searchOp = new InternalSearchOperation(
        conn, conn.nextOperationID(), conn.nextMessageID(), request, null);
    assertSuccess(hasProxyPrivilege, searchOp);
  }

  private void assertSuccess(boolean hasProxyPrivilege, CompareOperationBasis op)
  {
    op.run();
    if (hasProxyPrivilege)
    {
      assertEquals(op.getResultCode(), ResultCode.COMPARE_TRUE);
    }
    else
    {
      assertEquals(op.getResultCode(), ResultCode.AUTHORIZATION_DENIED);
    }
  }

  private void assertSuccess(boolean hasProxyPrivilege, Operation op)
  {
    op.run();
    if (hasProxyPrivilege)
    {
      assertEquals(op.getResultCode(), ResultCode.SUCCESS);
    }
    else
    {
      assertEquals(op.getResultCode(), ResultCode.AUTHORIZATION_DENIED);
    }
  }

  private void assertSuccess(boolean hasProxyPrivilege, DN userDN, Operation op)
  {
    op.run();
    if (hasProxyPrivilege)
    {
      assertEquals(op.getResultCode(), ResultCode.SUCCESS,
          "Unexpected failure for user " + userDN + " and operation " + op);
    }
    else
    {
      assertEquals(op.getResultCode(), ResultCode.AUTHORIZATION_DENIED,
          "Unexpected success for user " + userDN + " and operation " + op);
    }
  }

  /**
   * 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
  {
    final String dnStr = "cn=Test User,o=test";
    final DN dn = DN.valueOf(dnStr);
    Entry testEntry = TestCaseUtils.addEntry(
      "dn: " + dnStr,
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "cn: Test User",
      "givenName: Test",
      "sn: User",
      "userPassword: password");

    AuthenticationInfo authInfo = new AuthenticationInfo(testEntry, false);
    JmxConnectionHandler jmxCtx = getJmxConnectionHandler();
    JmxClientConnection testConnection =
         new JmxClientConnection(jmxCtx,authInfo);

    // 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 JMX_READ privilege and verify that
    // the client connection reflects that.
    processModify(dn, ADD, "ds-privilege-name", "jmx-read");
    assertTrue(testConnection.hasPrivilege(Privilege.JMX_READ, null));

    // Take the privilege away from the user and verify that it is recognized immediately.
    processModify(dn, DELETE, "ds-privilege-name", "jmx-read");
    assertFalse(testConnection.hasPrivilege(Privilege.JMX_READ, null));

    processDelete(dn);
  }

  /**
   * 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.
    JmxConnectionHandler jmxCtx = getJmxConnectionHandler();
    DN unprivRootDN = DN.valueOf("cn=Unprivileged Root,cn=Root DNs,cn=config");
    JmxClientConnection unprivRootConn = newJmxClientConnection(jmxCtx, unprivRootDN);
    assertFalse(unprivRootConn.hasPrivilege(Privilege.PROXIED_AUTH, null));

    DN rootDN = DN.valueOf("cn=Root DNs,cn=config");

    // Update the set of root privileges to include proxied auth.
    processModify(rootDN, ADD, "ds-cfg-default-root-privilege-name", "proxied-auth");

    // Get a new root connection and verify that it now has proxied auth.
    unprivRootConn = newJmxClientConnection(jmxCtx, unprivRootDN);
    assertTrue(unprivRootConn.hasPrivilege(Privilege.PROXIED_AUTH, null));

    // Update the set of root privileges to revoke proxied auth.
    processModify(rootDN, DELETE, "ds-cfg-default-root-privilege-name", "proxied-auth");

    unprivRootConn = newJmxClientConnection(jmxCtx, unprivRootDN);
    assertFalse(unprivRootConn.hasPrivilege(Privilege.PROXIED_AUTH, null));
  }

  private void processModify(DN dn, ModificationType modType, String attrName, String attrValue)
  {
    ArrayList<Modification> mods = newModifications(modType, attrName, attrValue);
    ModifyOperation modifyOp = getRootConnection().processModify(dn, mods);
    assertEquals(modifyOp.getResultCode(), ResultCode.SUCCESS);
  }

  private ArrayList<Modification> newModifications(ModificationType modType, String attrName, String attrValue)
  {
    return newArrayList(new Modification(modType, Attributes.create(attrName, attrValue)));
  }

  private void processDelete(DN entryDN)
  {
    DeleteOperation delOp = getRootConnection().processDelete(entryDN);
    assertEquals(delOp.getResultCode(), ResultCode.SUCCESS);
  }

  private JmxClientConnection newJmxClientConnection(JmxConnectionHandler jmxCtx, DN entryDN) throws DirectoryException
  {
    Entry entry = DirectoryServer.getEntry(entryDN);
    AuthenticationInfo authInfo = new AuthenticationInfo(entry, true);
    return new JmxClientConnection(jmxCtx, authInfo);
  }
}
