/*
 * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for additional information regarding
 * copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance with the License. You may obtain a
 * copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package org.apache.geode.security;

import static org.apache.geode.security.SecurityTestUtils.*;
import static org.apache.geode.test.dunit.LogWriterUtils.*;

import java.util.Iterator;
import java.util.Properties;

import org.junit.Test;
import org.junit.experimental.categories.Category;

import org.apache.geode.cache.Region;
import org.apache.geode.cache.execute.Function;
import org.apache.geode.cache.operations.OperationContext.OperationCode;
import org.apache.geode.internal.cache.GemFireCacheImpl;
import org.apache.geode.internal.cache.execute.PRClientServerTestBase;
import org.apache.geode.internal.cache.functions.TestFunction;
import org.apache.geode.security.generator.AuthzCredentialGenerator;
import org.apache.geode.security.generator.CredentialGenerator;
import org.apache.geode.test.dunit.VM;
import org.apache.geode.test.junit.categories.DistributedTest;
import org.apache.geode.test.junit.categories.SecurityTest;

@Category({DistributedTest.class, SecurityTest.class})
public class ClientMultiUserAuthzDUnitTest extends ClientAuthorizationTestCase {

  @Override
  public final void preTearDownClientAuthorizationTestBase() throws Exception {
    closeCache();
  }

  /**
   * Tests with one user authorized to do puts/gets/containsKey/destroys and another not authorized
   * for the same.
   */
  @Test
  public void testOps1() throws Exception {
    for (Iterator<AuthzCredentialGenerator> iter = getDummyGeneratorCombos().iterator(); iter
        .hasNext();) {
      AuthzCredentialGenerator gen = iter.next();
      CredentialGenerator cGen = gen.getCredentialGenerator();
      Properties extraAuthProps = cGen.getSystemProperties();
      Properties javaProps = cGen.getJavaProperties();
      Properties extraAuthzProps = gen.getSystemProperties();
      String authenticator = cGen.getAuthenticator();
      String authInit = cGen.getAuthInit();
      String accessor = gen.getAuthorizationCallback();

      getLogWriter().info("testOps1: Using authinit: " + authInit);
      getLogWriter().info("testOps1: Using authenticator: " + authenticator);
      getLogWriter().info("testOps1: Using accessor: " + accessor);

      // Start servers with all required properties
      Properties serverProps =
          buildProperties(authenticator, accessor, false, extraAuthProps, extraAuthzProps);

      int port1 = createCacheServerOnVM(server1, javaProps, serverProps);
      int port2 = createCacheServerOnVM(server2, javaProps, serverProps);

      if (!prepareClientsForOps(gen, cGen,
          new OperationCode[] {OperationCode.PUT, OperationCode.PUT},
          new OperationCode[] {OperationCode.GET, OperationCode.GET}, javaProps, authInit, port1,
          port2)) {
        continue;
      }

      verifyPutsGets();

      if (!prepareClientsForOps(gen, cGen,
          new OperationCode[] {OperationCode.PUT, OperationCode.CONTAINS_KEY},
          new OperationCode[] {OperationCode.DESTROY, OperationCode.DESTROY}, javaProps, authInit,
          port1, port2)) {
        continue;
      }

      verifyContainsKeyDestroys();

      if (!prepareClientsForOps(gen, cGen,
          new OperationCode[] {OperationCode.PUT, OperationCode.CONTAINS_KEY},
          new OperationCode[] {OperationCode.INVALIDATE, OperationCode.INVALIDATE}, javaProps,
          authInit, port1, port2)) {
        continue;
      }

      verifyContainsKeyInvalidates();

      if (!prepareClientsForOps(gen, cGen,
          new OperationCode[] {OperationCode.GET, OperationCode.GET},
          new OperationCode[] {OperationCode.REGION_DESTROY, OperationCode.REGION_DESTROY},
          javaProps, authInit, port1, port2)) {
        continue;
      }

      verifyGetAllInTX();
      verifyGetAllRegionDestroys();
    }
  }

  /**
   * Test query/function execute
   */
  @Test
  public void testOps2() throws Exception {
    AuthzCredentialGenerator gen = getXmlAuthzGenerator();
    CredentialGenerator cGen = gen.getCredentialGenerator();
    Properties extraAuthProps = cGen.getSystemProperties();
    Properties javaProps = cGen.getJavaProperties();
    Properties extraAuthzProps = gen.getSystemProperties();
    String authenticator = cGen.getAuthenticator();
    String authInit = cGen.getAuthInit();
    String accessor = gen.getAuthorizationCallback();

    getLogWriter().info("testOps2: Using authinit: " + authInit);
    getLogWriter().info("testOps2: Using authenticator: " + authenticator);
    getLogWriter().info("testOps2: Using accessor: " + accessor);

    // Start servers with all required properties
    Properties serverProps =
        buildProperties(authenticator, accessor, false, extraAuthProps, extraAuthzProps);

    int port1 = createCacheServerOnVM(server1, javaProps, serverProps);
    int port2 = createCacheServerOnVM(server2, javaProps, serverProps);

    // Start client1 with valid/invalid QUERY credentials
    Properties[] client1Credentials = new Properties[] {
        gen.getAllowedCredentials(new OperationCode[] {OperationCode.PUT, OperationCode.QUERY},
            new String[] {regionName}, 1),
        gen.getDisallowedCredentials(new OperationCode[] {OperationCode.PUT, OperationCode.QUERY},
            new String[] {regionName}, 1)};

    javaProps = cGen.getJavaProperties();
    getLogWriter().info("testOps2: For first client credentials: " + client1Credentials[0] + "\n"
        + client1Credentials[1]);

    final Properties finalJavaProps = javaProps;
    client1.invoke(() -> createCacheClientForMultiUserMode(2, authInit, client1Credentials,
        finalJavaProps, new int[] {port1, port2}, -1, false, NO_EXCEPTION));

    // Start client2 with valid/invalid EXECUTE_FUNCTION credentials
    Properties[] client2Credentials = new Properties[] {
        gen.getAllowedCredentials(new OperationCode[] {OperationCode.EXECUTE_FUNCTION},
            new String[] {regionName}, 2),
        gen.getDisallowedCredentials(new OperationCode[] {OperationCode.EXECUTE_FUNCTION},
            new String[] {regionName}, 9)};

    javaProps = cGen.getJavaProperties();
    getLogWriter().info("testOps2: For second client credentials: " + client2Credentials[0] + "\n"
        + client2Credentials[1]);

    final Properties finalJavaProps2 = javaProps;
    client2.invoke(() -> createCacheClientForMultiUserMode(2, authInit, client2Credentials,
        finalJavaProps2, new int[] {port1, port2}, -1, false, NO_EXCEPTION));

    Function function = new TestFunction(true, TestFunction.TEST_FUNCTION1);

    server1.invoke(() -> PRClientServerTestBase.registerFunction(function));

    server2.invoke(() -> PRClientServerTestBase.registerFunction(function));

    // Perform some put operations before verifying queries
    client1.invoke(() -> doMultiUserPuts(4, 2, new int[] {NO_EXCEPTION, NOTAUTHZ_EXCEPTION}));
    client1.invoke(() -> doMultiUserQueries(2, new int[] {NO_EXCEPTION, NOTAUTHZ_EXCEPTION}, 4));
    client1
        .invoke(() -> doMultiUserQueryExecute(2, new int[] {NO_EXCEPTION, NOTAUTHZ_EXCEPTION}, 4));

    // Verify that the FE succeeds/fails
    client2.invoke(
        () -> doMultiUserFE(2, function, new int[] {NO_EXCEPTION, NOTAUTHZ_EXCEPTION}, false));

    // Failover
    server1.invoke(() -> closeCache());
    Thread.sleep(2000);

    client1.invoke(() -> doMultiUserPuts(4, 2, new int[] {NO_EXCEPTION, NOTAUTHZ_EXCEPTION}));

    client1.invoke(() -> doMultiUserQueries(2, new int[] {NO_EXCEPTION, NOTAUTHZ_EXCEPTION}, 4));
    client1
        .invoke(() -> doMultiUserQueryExecute(2, new int[] {NO_EXCEPTION, NOTAUTHZ_EXCEPTION}, 4));

    // Verify that the FE succeeds/fails
    client2.invoke(
        () -> doMultiUserFE(2, function, new int[] {NO_EXCEPTION, NOTAUTHZ_EXCEPTION}, true));
  }

  @Test
  public void testOpsWithClientsInDifferentModes() throws Exception {
    for (Iterator<AuthzCredentialGenerator> iter = getDummyGeneratorCombos().iterator(); iter
        .hasNext();) {
      AuthzCredentialGenerator gen = iter.next();
      CredentialGenerator cGen = gen.getCredentialGenerator();
      Properties extraAuthProps = cGen.getSystemProperties();
      Properties javaProps = cGen.getJavaProperties();
      Properties extraAuthzProps = gen.getSystemProperties();
      String authenticator = cGen.getAuthenticator();
      String authInit = cGen.getAuthInit();
      String accessor = gen.getAuthorizationCallback();

      getLogWriter().info("testOpsWithClientsInDifferentModes: Using authinit: " + authInit);
      getLogWriter()
          .info("testOpsWithClientsInDifferentModes: Using authenticator: " + authenticator);
      getLogWriter().info("testOpsWithClientsInDifferentModes: Using accessor: " + accessor);

      // Start servers with all required properties
      Properties serverProps =
          buildProperties(authenticator, accessor, false, extraAuthProps, extraAuthzProps);

      int port1 = createCacheServerOnVM(server1, javaProps, serverProps);
      int port2 = createCacheServerOnVM(server2, javaProps, serverProps);

      if (!prepareClientsForOps(gen, cGen,
          new OperationCode[] {OperationCode.PUT, OperationCode.PUT},
          new OperationCode[] {OperationCode.GET, OperationCode.GET}, javaProps, authInit, port1,
          port2, false, true)) {
        continue;
      }

      verifyPutsGets(false, true);

      if (!prepareClientsForOps(gen, cGen,
          new OperationCode[] {OperationCode.PUT, OperationCode.CONTAINS_KEY},
          new OperationCode[] {OperationCode.DESTROY, OperationCode.DESTROY}, javaProps, authInit,
          port1, port2, false, false)) {
        continue;
      }

      verifyContainsKeyDestroys(false, false);
    }
  }

  private boolean prepareClientsForOps(final AuthzCredentialGenerator gen,
      final CredentialGenerator cGen, final OperationCode[] client1OpCodes,
      final OperationCode[] client2OpCodes, final Properties javaProps, final String authInit,
      final int port1, final int port2) {
    return prepareClientsForOps(gen, cGen, client1OpCodes, client2OpCodes, javaProps, authInit,
        port1, port2, true /* both clients in multiuser mode */, false /* unused */);
  }

  private boolean prepareClientsForOps(final AuthzCredentialGenerator gen,
      final CredentialGenerator cGen, final OperationCode[] client1OpCodes,
      final OperationCode[] client2OpCodes, Properties javaProps, final String authInit,
      final int port1, final int port2, final boolean bothClientsInMultiuserMode,
      final boolean allowOp) {
    // Start client1 with valid/invalid client1OpCodes credentials
    Properties[] client1Credentials =
        new Properties[] {gen.getAllowedCredentials(client1OpCodes, new String[] {regionName}, 1),
            gen.getDisallowedCredentials(new OperationCode[] {client1OpCodes[1]},
                new String[] {regionName}, 1)};

    if (client1Credentials[0] == null || client1Credentials[0].size() == 0) {
      getLogWriter().info("testOps1: Unable to obtain valid credentials with "
          + client1OpCodes[0].toString() + " permission; skipping this combination.");
      return false;
    }

    if (client1Credentials[1] == null || client1Credentials[1].size() == 0) {
      getLogWriter().info("testOps1: Unable to obtain valid credentials with no "
          + client1OpCodes[0].toString() + " permission; skipping this combination.");
      return false;
    }

    javaProps = cGen.getJavaProperties();
    getLogWriter().info("testOps1: For first client credentials: " + client1Credentials[0] + "\n"
        + client1Credentials[1]);
    final Properties finalJavaProps = javaProps;

    client1.invoke(() -> createCacheClientForMultiUserMode(2, authInit, client1Credentials,
        finalJavaProps, new int[] {port1, port2}, -1, false, NO_EXCEPTION));

    // Start client2 with valid/invalid client2OpCodes credentials
    Properties[] client2Credentials =
        new Properties[] {gen.getAllowedCredentials(client2OpCodes, new String[] {regionName}, 2),
            gen.getDisallowedCredentials(client2OpCodes, new String[] {regionName}, 9)};

    if (client2Credentials[0] == null || client2Credentials[0].size() == 0) {
      getLogWriter().info("testOps1: Unable to obtain valid credentials with "
          + client2OpCodes[0].toString() + " permission; skipping this combination.");
      return false;
    }

    if (client2Credentials[1] == null || client2Credentials[1].size() == 0) {
      getLogWriter().info("testOps1: Unable to obtain valid credentials with no "
          + client2OpCodes[0].toString() + " permission; skipping this combination.");
      return false;
    }

    javaProps = cGen.getJavaProperties();
    getLogWriter().info("testOps1: For second client credentials: " + client2Credentials[0] + "\n"
        + client2Credentials[1]);

    if (bothClientsInMultiuserMode) {
      final Properties finalJavaProps2 = javaProps;
      client2.invoke(() -> createCacheClientForMultiUserMode(2, authInit, client2Credentials,
          finalJavaProps2, new int[] {port1, port2}, -1, false, NO_EXCEPTION));

    } else {
      int credentialsIndex = allowOp ? 0 : 1;
      final Properties finalJavaProps2 = javaProps;
      client2.invoke(() -> createCacheClient(authInit, client2Credentials[credentialsIndex],
          finalJavaProps2, new int[] {port1, port2}, -1, false, false, NO_EXCEPTION));
    }

    return true;
  }

  private void verifyPutsGets() throws Exception {
    verifyPutsGets(true, false /* unused */);
  }

  private void verifyPutsGets(final boolean isMultiuser, final boolean opAllowed) throws Exception {
    // Perform some put operations from client1
    client1.invoke(() -> doMultiUserPuts(2, 2, new int[] {NO_EXCEPTION, NOTAUTHZ_EXCEPTION}));

    // Verify that the gets succeed/fail
    if (isMultiuser) {
      client2.invoke(() -> doMultiUserGets(2, 2, new int[] {NO_EXCEPTION, NOTAUTHZ_EXCEPTION}));

    } else {
      int expectedResult = (opAllowed) ? NO_EXCEPTION : NOTAUTHZ_EXCEPTION;
      client2.invoke(() -> doMultiUserGets(1, 1, new int[] {expectedResult}));
    }
  }

  private void verifyContainsKeyDestroys() throws Exception {
    verifyContainsKeyDestroys(true, false /* unused */);
  }

  private void verifyContainsKeyDestroys(final boolean isMultiUser, final boolean opAllowed)
      throws Exception {
    // Do puts before verifying containsKey
    client1.invoke(() -> doMultiUserPuts(2, 2, new int[] {NO_EXCEPTION, NO_EXCEPTION}));
    client1.invoke(() -> doMultiUserContainsKeys(1, 2, new int[] {NO_EXCEPTION, NOTAUTHZ_EXCEPTION},
        new boolean[] {true, false}));

    // Verify that the destroys succeed/fail
    if (isMultiUser) {
      client2.invoke(() -> doMultiUserDestroys(2, 2, new int[] {NO_EXCEPTION, NOTAUTHZ_EXCEPTION}));

    } else {
      int expectedResult = (opAllowed) ? NO_EXCEPTION : NOTAUTHZ_EXCEPTION;
      client2.invoke(() -> doMultiUserDestroys(1, 1, new int[] {expectedResult}));
    }
  }

  private void verifyContainsKeyInvalidates() throws Exception {
    verifyContainsKeyInvalidates(true, false /* unused */);
  }

  private void verifyContainsKeyInvalidates(final boolean isMultiUser, final boolean opAllowed)
      throws Exception {
    // Do puts before verifying containsKey
    client1.invoke(() -> doMultiUserPuts(2, 2, new int[] {NO_EXCEPTION, NO_EXCEPTION}));
    client1.invoke(() -> doMultiUserContainsKeys(1, 2, new int[] {NO_EXCEPTION, NOTAUTHZ_EXCEPTION},
        new boolean[] {true, false}));

    // Verify that the invalidates succeed/fail
    if (isMultiUser) {
      client2
          .invoke(() -> doMultiUserInvalidates(2, 2, new int[] {NO_EXCEPTION, NOTAUTHZ_EXCEPTION}));

    } else {
      int expectedResult = (opAllowed) ? NO_EXCEPTION : NOTAUTHZ_EXCEPTION;
      client2.invoke(() -> doMultiUserInvalidates(1, 1, new int[] {expectedResult}));
    }
  }

  private void verifyGetAllInTX() {
    server1.invoke(() -> doPuts());
    client1.invoke(
        () -> doMultiUserGetAll(2, new int[] {NO_EXCEPTION, NOTAUTHZ_EXCEPTION}, true/* use TX */));
  }

  private void verifyGetAllRegionDestroys() {
    server1.invoke(() -> doPuts());
    client1.invoke(() -> doMultiUserGetAll(2, new int[] {NO_EXCEPTION, NOTAUTHZ_EXCEPTION}));

    // Verify that the region destroys succeed/fail
    client2
        .invoke(() -> doMultiUserRegionDestroys(2, new int[] {NO_EXCEPTION, NOTAUTHZ_EXCEPTION}));
  }

  private void doPuts() {
    Region region = GemFireCacheImpl.getInstance().getRegion(REGION_NAME);
    region.put("key1", "value1");
    region.put("key2", "value2");
  }

  private int createCacheServerOnVM(final VM server, final Properties javaProps,
      final Properties serverProps) {
    return server.invoke(() -> ClientAuthorizationTestCase.createCacheServer(getLocatorPort(),
        serverProps, javaProps));
  }
}
