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

import static org.testng.Assert.*;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.ServerSocket;
import java.security.KeyStore;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

import javax.management.Attribute;
import javax.management.AttributeNotFoundException;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;
import javax.net.ssl.TrustManagerFactory;

import org.forgerock.i18n.LocalizableMessage;
import org.opends.server.TestCaseUtils;
import org.opends.server.admin.server.AdminTestCaseUtils;
import org.opends.server.admin.std.meta.JMXConnectionHandlerCfgDefn;
import org.opends.server.admin.std.server.JMXConnectionHandlerCfg;
import org.opends.server.config.JMXMBean;
import org.opends.server.core.AddOperationBasis;
import org.opends.server.core.DeleteOperation;
import org.opends.server.core.DeleteOperationBasis;
import org.opends.server.core.DirectoryServer;
import org.opends.server.protocols.internal.InternalClientConnection;
import org.forgerock.opendj.config.server.ConfigChangeResult;
import org.opends.server.types.DN;
import org.opends.server.types.Entry;
import org.forgerock.opendj.ldap.ResultCode;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

/**
 * A simple test for : - JMX connection establishment without using SSL -
 * JMX get and set - configuration change
 */
@SuppressWarnings("javadoc")
public class JmxConnectTest extends JmxTestCase {

  /**
   * Set up the environment for performing the tests in this suite.
   *
   * @throws Exception
   *           If the environment could not be set up.
   */
  @Override
  @BeforeClass
  public void setUp() throws Exception
  {
    super.setUp();

    TestCaseUtils.addEntries(
        "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-pwp-password-policy-dn: cn=Clear UserPassword Policy," +
             "cn=Password Policies,cn=config",
        "",
        "dn: cn=Unprivileged JMX 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-pwp-password-policy-dn: cn=Clear UserPassword Policy," +
             "cn=Password Policies,cn=config");
  }


  /**
   * Clean up the environment after performing the tests in this suite.
   *
   * @throws Exception
   *           If the environment could not be set up.
   */
  @AfterClass
  public void afterClass() throws Exception
  {
    InternalClientConnection conn = InternalClientConnection
        .getRootConnection();

    DeleteOperation deleteOperation = conn.processDelete(DN
        .valueOf("cn=Privileged User,o=test"));
    assertEquals(deleteOperation.getResultCode(), ResultCode.SUCCESS);

    deleteOperation = conn.processDelete(DN
        .valueOf("cn=Unprivileged JMX User,o=test"));
    assertEquals(deleteOperation.getResultCode(), ResultCode.SUCCESS);
  }



  /**
   * Build data for the simpleConnect test.
   *
   * @return the data.
   */
  @DataProvider(name = "simpleConnect")
  Object[][] createCredentials() {
    return new Object[][] {
        { "cn=directory manager", "password", false }, // no JMX_READ privilege
        { "cn=Privileged User,o=test", "password", true },
        { "cn=Privileged User,o=test", "wrongPassword", false },
        { "cn=wrong user", "password", false },
        { "invalid DN", "password", false },
        { "cn=Privileged User,o=test", null, false },
        { null, "password", false }, { null, null, false }, };
  }



  /**
   * Check that simple (no SSL) connections to the JMX service are
   * accepted when the given credentials are OK and refused when the
   * credentials are invalid.
   */
  @Test(enabled = false, dataProvider = "simpleConnect")
  public void simpleConnect(String user, String password,
      boolean expectedResult) throws Exception {
    OpendsJmxConnector connector = connect(user, password,
        TestCaseUtils.getServerJmxPort());
    assertEquals(connector != null, expectedResult);
    if (connector != null) {
      connector.close();
    }
  }

  /**
   * Build some data for the simpleGet test.
   */
  @DataProvider(name = "simpleGet")
  Object[][] createNames() {
    return new Object[][] {
        {
            "cn=JMX Connection Handler,cn=Connection Handlers,cn=config",
            "ds-cfg-listen-port", TestCaseUtils.getServerJmxPort() },
        {
            "cn=JMX Connection Handler,cn=Connection Handlers,cn=config",
            "objectclass", null },
        {
            "cn=JMX Connection Handler,cn=Connection Handlers,cn=config",
            "ds-cfg-ssl-cert-nickname", "server-cert" },
    // not working at the moment see issue 655
    // {"cn=JE
    // Database,ds-cfg-backend-id=userRoot,cn=Backends,cn=config",
    // "ds-cfg-db-cache-percent", 10},
    };
  }



  /**
   * Test simple JMX get.
   */
  @Test(enabled = false, dataProvider = "simpleGet")
  public void simpleGet(String dn, String attributeName, Object value)
      throws Exception {

    OpendsJmxConnector connector = connect("cn=Privileged User,o=test",
        "password", TestCaseUtils.getServerJmxPort());
    MBeanServerConnection jmxc = connector.getMBeanServerConnection();
    assertNotNull(jmxc);

    Object val = jmxGet(dn, attributeName, jmxc);

    if (value != null) {
      assertEquals(val, value);
    } else {
      assertNull(val);
    }
    connector.close();
  }



  /**
   * Test setting some config attribute through jmx.
   */
  // NOTE:  This test is disabled because JMX interaction is not allowed with
  //        the admin framework, and the cn=config entry is now governed by it.
  @Test(enabled = false)
  public void simpleSet() throws Exception {
    OpendsJmxConnector connector = connect("cn=Privileged User,o=test",
        "password", TestCaseUtils.getServerJmxPort());
    MBeanServerConnection jmxc = connector.getMBeanServerConnection();
    assertNotNull(jmxc);

    jmxc.queryNames(null, null).clear();

    final String dn = "cn=config";
    final String attribute = "ds-cfg-size-limit";

    long val = (Long) jmxGet(dn, attribute, jmxc);
    jmxSet(dn, attribute, val + 1, jmxc);

    long newVal = (Long) jmxGet(dn, attribute, jmxc);
    assertEquals(newVal, val + 1);

    jmxSet(dn, attribute, val + 1, jmxc);

    connector.close();
  }



  /**
   * Test that disabling JMX connection handler does its job by -
   * opening a JMX connection - changing the JMX connection handler
   * state to disable - trying to open a new JMX connection and check
   * that it fails.
   *
   * @throws Exception
   */
  @Test(enabled = false)
  public void disable() throws Exception {

    // Create a new JMX connector for this test.
    // This will allow to use the old one if this test fails.
    ServerSocket serverJmxSocket = new ServerSocket();
    serverJmxSocket.setReuseAddress(true);
    serverJmxSocket.bind(new InetSocketAddress("127.0.0.1", 0));
    int serverJmxPort = serverJmxSocket.getLocalPort();
    serverJmxSocket.close();
    Entry newJmxConnectionJmx = TestCaseUtils
        .makeEntry(
            "dn: cn= New JMX Connection Handler,cn=Connection Handlers,cn=config",
            "objectClass: top",
            "objectClass: ds-cfg-connection-handler",
            "objectClass: ds-cfg-jmx-connection-handler",
            "ds-cfg-ssl-cert-nickname: server-cert",
            "ds-cfg-java-class: org.opends.server.protocols.jmx.JmxConnectionHandler",
            "ds-cfg-enabled: true",
            "ds-cfg-use-ssl: false", "ds-cfg-listen-port: "
                + serverJmxPort, "cn: JMX Connection Handler");
    InternalClientConnection connection =
      InternalClientConnection.getRootConnection();
    AddOperationBasis addOp = new AddOperationBasis(connection,
        InternalClientConnection.nextOperationID(),
        InternalClientConnection.nextMessageID(), null,
        newJmxConnectionJmx.getName(), newJmxConnectionJmx
            .getObjectClasses(), newJmxConnectionJmx
            .getUserAttributes(), newJmxConnectionJmx
            .getOperationalAttributes());
    addOp.run();
    Thread.sleep(200);
    OpendsJmxConnector newJmxConnector = connect(
        "cn=Privileged User,o=test", "password", serverJmxPort);
    assertNotNull(newJmxConnector);
    newJmxConnector.close();

    // Get the "old" connector
    OpendsJmxConnector connector = connect("cn=Privileged User,o=test",
        "password", TestCaseUtils.getServerJmxPort());
    MBeanServerConnection jmxc = connector.getMBeanServerConnection();
    assertNotNull(jmxc);

    // Disable the "new" connector
    toggleEnableJmxConnector(connector, newJmxConnectionJmx.getName(), false);
    Thread.sleep(100);
    OpendsJmxConnector jmxcDisabled = connect("cn=Privileged User,o=test",
        "password", serverJmxPort);
    assertNull(jmxcDisabled);

    toggleEnableJmxConnector(connector, newJmxConnectionJmx.getName(), true);
    Thread.sleep(100);
    jmxcDisabled = connect("cn=Privileged User,o=test", "password", serverJmxPort);
    assertNotNull(jmxcDisabled);

    // cleanup client connection
    connector.close();
    jmxcDisabled.close();
    DeleteOperationBasis delOp = new DeleteOperationBasis(connection,
        InternalClientConnection.nextOperationID(),
        InternalClientConnection.nextMessageID(), null,
        newJmxConnectionJmx.getName());
    delOp.run();
  }



  /**
   * Test changing JMX port through LDAP.
   */
  @Test(enabled = false)
  public void changePort() throws Exception {
    // Connect to the JMX service and get the current port
    final String dn = "cn=JMX Connection Handler,cn=Connection Handlers,cn=config";
    final String attribute = "ds-cfg-listen-port";

    OpendsJmxConnector connector = connect("cn=Privileged User,o=test",
        "password", TestCaseUtils.getServerJmxPort());
    MBeanServerConnection jmxc = connector.getMBeanServerConnection();
    assertNotNull(jmxc);

    // use JMX to get the current value of the JMX port number
    Long initJmxPort = (Long) jmxGet(dn, attribute, jmxc);
    connector.close();
    assertNotNull(initJmxPort);

    // change the configuration of the connection handler to use a free port
    int serverJmxPort = TestCaseUtils.findFreePort();

    Entry entry = TestCaseUtils
            .makeEntry(
                "dn: cn=JMX Connection Handler,cn=Connection Handlers,cn=config",
                "objectClass: top",
                "objectClass: ds-cfg-connection-handler",
                "objectClass: ds-cfg-jmx-connection-handler",
                "ds-cfg-ssl-cert-nickname: server-cert",
                "ds-cfg-java-class: org.opends.server.protocols.jmx.JmxConnectionHandler",
                "ds-cfg-enabled: true",
                "ds-cfg-use-ssl: false", "ds-cfg-listen-port: "
                    + serverJmxPort, "cn: JMX Connection Handler");
    configureJmx(entry);

    // connect the the JMX service using the new port
    connector = connect("cn=Privileged User,o=test", "password", serverJmxPort);
    jmxc = connector.getMBeanServerConnection();
    assertNotNull(jmxc);
    long val = (Long) jmxGet(dn, attribute, jmxc);
    assertEquals(val, serverJmxPort);
    connector.close();

    // re-establish the initial configuration of the JMX service
    entry = TestCaseUtils
            .makeEntry(
                "dn: cn=JMX Connection Handler,cn=Connection Handlers,cn=config",
                "objectClass: top",
                "objectClass: ds-cfg-connection-handler",
                "objectClass: ds-cfg-jmx-connection-handler",
                "ds-cfg-ssl-cert-nickname: server-cert",
                "ds-cfg-java-class: org.opends.server.protocols.jmx.JmxConnectionHandler",
                "ds-cfg-enabled: true",
                "ds-cfg-use-ssl: false", "ds-cfg-listen-port: "
                    + initJmxPort, "cn: JMX Connection Handler");

    configureJmx(entry);

    // Check that the old port is ok
    connector = connect("cn=Privileged User,o=test", "password",
        TestCaseUtils.getServerJmxPort());
    jmxc = connector.getMBeanServerConnection();
    assertNotNull(jmxc);
  }



  /**
   * Check that simple (no SSL) connections to the JMX service are
   * accepted when the given credentials are OK and refused when the
   * credentials are invalid.
   */
  @Test(enabled = false) // this fails a lot especially due to the in core restarts
  public void sslConnect() throws Exception {
    // Enable SSL by setting ds-cfg-use-ssl boolean and the
    // certificate alias using ds-cfg-ssl-cert-nickname attribute.
    int initJmxPort = TestCaseUtils.getServerJmxPort();

    Entry entry = TestCaseUtils
        .makeEntry(
            "dn: cn=JMX Connection Handler,cn=Connection Handlers,cn=config",
            "objectClass: top",
            "objectClass: ds-cfg-connection-handler",
            "objectClass: ds-cfg-jmx-connection-handler",
            "ds-cfg-ssl-cert-nickname: server-cert",
            "ds-cfg-java-class: org.opends.server.protocols.jmx.JmxConnectionHandler",
            "ds-cfg-enabled: true",
            "ds-cfg-use-ssl: true",
            "ds-cfg-listen-port: " + initJmxPort,
            "ds-cfg-key-manager-provider: cn=JKS,cn=Key Manager Providers,cn=config",
            "cn: JMX Connection Handler");

    configureJmx(entry);

    OpendsJmxConnector jmxc = sslConnect("cn=Privileged User,o=test",
        "password", initJmxPort);
    assertNotNull(jmxc,"OpendsJmxConnector shouldn't be null");
    jmxc.getMBeanServerConnection();
    jmxc.close();

    // Before returning the result,
    // disable SSL by setting ds-cfg-use-ssl boolean
    entry = TestCaseUtils
            .makeEntry(
                "dn: cn=JMX Connection Handler,cn=Connection Handlers,cn=config",
                "objectClass: top",
                "objectClass: ds-cfg-connection-handler",
                "objectClass: ds-cfg-jmx-connection-handler",
                "ds-cfg-ssl-cert-nickname: server-cert",
                "ds-cfg-java-class: org.opends.server.protocols.jmx.JmxConnectionHandler",
                "ds-cfg-enabled: true",
                "ds-cfg-use-ssl: false", "ds-cfg-listen-port: "
                    + initJmxPort, "cn: JMX Connection Handler");
    try {
      configureJmx(entry);
    } catch (RuntimeException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }

    jmxc = connect("cn=Privileged User,o=test", "password", initJmxPort);
    jmxc.close();
    assertNotNull(jmxc);
  }



  private void configureJmx(Entry entry) throws Exception {
    ArrayList<LocalizableMessage> reasons = new ArrayList<>();

    // Get the Jmx connection handler from the core server
    JmxConnectionHandler jmxConnectionHandler = getJmxConnectionHandler();
    assertNotNull(jmxConnectionHandler);

    JMXConnectionHandlerCfg config = AdminTestCaseUtils
        .getConfiguration(JMXConnectionHandlerCfgDefn
            .getInstance(), entry);

    if (!jmxConnectionHandler.isConfigurationChangeAcceptable(config,
        reasons)) {
      fail("unacceptable JMX configuration" + reasons);
    }

    ConfigChangeResult configResult = jmxConnectionHandler
        .applyConfigurationChange(config);

    assertEquals(configResult.getResultCode(), ResultCode.SUCCESS);
  }

  /**
   * Connect to the JMX service.
   */
  private OpendsJmxConnector connect(String user, String password, int jmxPort)
        throws MalformedURLException, IOException {
    Map<String, Object> env = new HashMap<>();

    // Provide the credentials required by the server to successfully
    // perform user authentication
    String[] credentials;
    if (user == null && password == null) {
      credentials = null;
    }
    else
    {
      credentials = new String[] { user, password };
    }
    env.put("jmx.remote.credentials", credentials);

    env.put("jmx.remote.x.client.connection.check.period", 0);

    // Create an RMI connector client and
    // connect it to the RMI connector server
    OpendsJmxConnector opendsConnector;
    try {
      opendsConnector = new OpendsJmxConnector("localhost", jmxPort, env);
      opendsConnector.connect();
      return opendsConnector;
    } catch (SecurityException e) {
      return null;
    } catch (IOException e) {
      return null;
    }
  }



  /**
   * Connect to the JMX service using SSL.
   */
  private OpendsJmxConnector sslConnect(String user, String password,
      long jmxPort) throws Exception {
    Map<String, Object> env = new HashMap<>();

    // Provide the credentials required by the server to successfully
    // perform user authentication
    String[] credentials;
    if (user == null && password == null) {
      credentials = null;
    }
    else
    {
      credentials = new String[] { user, password };
    }
    env.put("jmx.remote.credentials", credentials);

    // Provide the Trust manager.
    KeyStore ks = KeyStore.getInstance("JKS");
    FileInputStream keyStoreFile = new FileInputStream(getJmxKeystorePath());
    ks.load(keyStoreFile, "password".toCharArray());
    keyStoreFile.close();

    TrustManagerFactory tmf = TrustManagerFactory
        .getInstance(TrustManagerFactory.getDefaultAlgorithm());
    tmf.init(ks);
    env.put(JmxConnectionHandler.TRUST_MANAGER_ARRAY_KEY, tmf.getTrustManagers());

    // Create an RMI connector client and
    // connect it to the RMI connector server
    OpendsJmxConnector opendsConnector;
    try {
      opendsConnector = new OpendsJmxConnector("localhost",
          (int) jmxPort, env);
      opendsConnector.connect();
      return opendsConnector;
    } catch (Exception e) {
      return null;
    }
  }



  private String getJmxKeystorePath() {
    return DirectoryServer.getInstanceRoot() + File.separator + "config"
        + File.separator + "server.keystore";
  }



  /**
   * Set the enabled config attribute for a JMX connector thorugh JMX
   * operation.
   *
   * @param jmxc
   *          connector to use for the interaction
   * @param testedConnector
   *          The DN of the connector the test
   * @param enabled
   *          the value of the enabled config attribute
   */
  private void toggleEnableJmxConnector(OpendsJmxConnector jmxc,
      DN testedConnector, boolean enabled) throws Exception {
    MBeanServerConnection mbsc = jmxc.getMBeanServerConnection();

    // Get status of the JMX connection handler
    String jmxName = JMXMBean.getJmxName(testedConnector);
    ObjectName name = ObjectName.getInstance(jmxName);
    Attribute status = (Attribute) mbsc.getAttribute(name,
        "ds-cfg-enabled");
    if (status != null)
    {
      status.getValue();
    }
    Attribute attr = new Attribute(
        "ds-cfg-enabled", enabled);
    mbsc.setAttribute(name, attr);
    status = (Attribute) mbsc.getAttribute(name,
        "ds-cfg-enabled");

    status = null;
  }



  /**
   * Get an attribute value through JMX.
   */
  private Object jmxGet(String dn, String attributeName,
      MBeanServerConnection mbsc) throws Exception {
    String jmxName = JMXMBean.getJmxName(DN.valueOf(dn));
    ObjectName name = ObjectName.getInstance(jmxName);

    try
    {
      Attribute status = (Attribute) mbsc.getAttribute(name, attributeName);
      if (status != null)
      {
        return status.getValue();
      }
      return null;
    }
    catch (AttributeNotFoundException anfe)
    {
      return null;
    }
  }



  /**
   * Set an attribute value through JMX.
   */
  private void jmxSet(String dn, String attributeName, Object value,
      MBeanServerConnection mbsc) throws Exception {
    String jmxName = JMXMBean.getJmxName(DN.valueOf(dn));
    ObjectName name = ObjectName.getInstance(jmxName);
    Attribute attr = new Attribute(attributeName, value);

    mbsc.setAttribute(name, attr);
  }

}
