/*
 * 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.closeCache;
import static org.apache.geode.security.SecurityTestUtils.getLocatorPort;

import org.apache.geode.DataSerializable;
import org.apache.geode.Instantiator;
import org.apache.geode.cache.operations.OperationContext.OperationCode;
import org.apache.geode.distributed.ConfigurationProperties;
import org.apache.geode.internal.AvailablePortHelper;
import org.apache.geode.internal.security.FilterPostAuthorization;
import org.apache.geode.internal.security.FilterPreAuthorization;
import org.apache.geode.internal.security.ObjectWithAuthz;
import org.apache.geode.security.generator.CredentialGenerator;
import org.apache.geode.security.generator.DummyAuthzCredentialGenerator;
import org.apache.geode.security.generator.DummyCredentialGenerator;
import org.apache.geode.security.templates.UserPasswordAuthInit;
import org.apache.geode.test.junit.categories.DistributedTest;
import org.apache.geode.test.junit.categories.FlakyTest;
import org.apache.geode.test.junit.categories.SecurityTest;
import org.junit.Test;
import org.junit.experimental.categories.Category;

import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.Random;

/**
 * Tests for authorization callback that modify objects and callbacks from client to server.
 * 
 * The scheme of these tests is thus: A user name terminating in integer 'i' can get values
 * terminating with 'i', '2*i', '3*i' and so on. So 'gemfire1' can get 'value1', 'value2', ...;
 * 'gemfire2' can get 'value2', 'value4', ... and so on. On the server side this is done by adding
 * the index 'i' to the object in the pre-processing phase, and checked by comparing against the
 * user name index during the post-processing phase.
 * 
 * This enables testing of object and callback modification both in pre-processing and
 * post-processing phases.
 * 
 * @since GemFire 5.5
 */
@Category({DistributedTest.class, SecurityTest.class})
public class ClientAuthzObjectModDUnitTest extends ClientAuthorizationTestCase {

  private static final String PRE_ACCESSOR_CREATE =
      FilterPreAuthorization.class.getName() + ".create";
  private static final String POST_ACCESSOR_CREATE =
      FilterPostAuthorization.class.getName() + ".create";

  @Override
  protected final void postSetUpClientAuthorizationTestBase() throws Exception {
    // required by FilterPreAuthorization and FilterPostAuthorization. Normally,
    // this would be automatically registered in the static initializer, but with dunit
    // a previous test may have already loaded these classes. We clear the instantiators
    // between each test.
    server1.invoke("registerInstantiator",
        () -> Instantiator.register(new MyInstantiator(), false));
    server2.invoke("registerInstantiator",
        () -> Instantiator.register(new MyInstantiator(), false));
  }

  @Category(FlakyTest.class) // GEODE-2071: failed with ForcedDisconnectException
  @Test
  public void testAllOpsObjectModWithFailover() throws Exception {
    OperationWithAction[] allOps = allOps();

    TestPostCredentialGenerator tgen = new TestPostCredentialGenerator();

    CredentialGenerator credentialGenerator = new DummyCredentialGenerator();
    credentialGenerator.init();
    Properties extraProps = credentialGenerator.getSystemProperties();
    Properties javaProps = credentialGenerator.getJavaProperties();
    String authInit = credentialGenerator.getAuthInit();
    String authenticator = credentialGenerator.getAuthenticator();

    System.out.println("testPutsGetsObjectModWithFailover: Using authinit: " + authInit);
    System.out.println("testPutsGetsObjectModWithFailover: Using authenticator: " + authenticator);
    System.out.println(
        "testPutsGetsObjectModWithFailover: Using pre-operation accessor: " + PRE_ACCESSOR_CREATE);
    System.out.println("testPutsGetsObjectModWithFailover: Using post-operation accessor: "
        + POST_ACCESSOR_CREATE);

    // Start servers with all required properties
    Properties serverProps =
        buildProperties(authenticator, extraProps, PRE_ACCESSOR_CREATE, POST_ACCESSOR_CREATE);

    // Get ports for the servers
    int[] portsList = AvailablePortHelper.getRandomAvailableTCPPorts(2);
    int port1 = 0; // portsList[0];
    int port2 = portsList[1];

    // Perform all the ops on the clients
    List<OperationWithAction> opBlock = new ArrayList();
    Random rnd = new Random();

    for (int opNum = 0; opNum < allOps.length; ++opNum) {
      // Start client with valid credentials as specified in OperationWithAction
      OperationWithAction currentOp = allOps[opNum];
      if (currentOp.equals(OperationWithAction.OPBLOCK_END)
          || currentOp.equals(OperationWithAction.OPBLOCK_NO_FAILOVER)) {
        // End of current operation block; execute all the operations on the servers with failover
        if (opBlock.size() > 0) {
          // Start the first server and execute the operation block
          port1 = server1.invoke("createCacheServer",
              () -> createCacheServer(getLocatorPort(), 0, serverProps, javaProps));
          server2.invoke("closeCache", () -> closeCache());
          executeOpBlock(opBlock, port1, port2, authInit, extraProps, null, tgen, rnd);
          if (!currentOp.equals(OperationWithAction.OPBLOCK_NO_FAILOVER)) {
            // Failover to the second server and run the block again
            port2 = server2.invoke("createCacheServer",
                () -> createCacheServer(getLocatorPort(), 0, serverProps, javaProps));
            server1.invoke("closeCache", () -> closeCache());
            executeOpBlock(opBlock, port1, port2, authInit, extraProps, null, tgen, rnd);
          }
          opBlock.clear();
        }

      } else {
        currentOp.setOpNum(opNum);
        opBlock.add(currentOp);
      }
    }
  }

  private OperationWithAction[] allOps() {
    return new OperationWithAction[] {
        // Perform CREATE and verify with GET
        new OperationWithAction(OperationCode.PUT, 1, OpFlags.NONE, 8),
        // For second client connect with valid credentials for key2, key4, key6, key8 and check
        // that other KEYS are not accessible
        new OperationWithAction(OperationCode.GET, 2, OpFlags.CHECK_NOKEY, new int[] {1, 3, 5, 7}),
        new OperationWithAction(OperationCode.GET, 2,
            OpFlags.CHECK_NOKEY | OpFlags.USE_OLDCONN | OpFlags.CHECK_NOTAUTHZ,
            new int[] {0, 2, 4, 6}),
        // For third client check that key3, key6 are accessible but others are not
        new OperationWithAction(OperationCode.GET, 3, OpFlags.CHECK_NOKEY, new int[] {2, 5}),
        new OperationWithAction(OperationCode.GET, 3,
            OpFlags.CHECK_NOKEY | OpFlags.USE_OLDCONN | OpFlags.CHECK_NOTAUTHZ,
            new int[] {0, 1, 3, 4, 6, 7}),

        // OPBLOCK_END indicates end of an operation block that needs to be executed on each server
        // when doing failover
        OperationWithAction.OPBLOCK_END,

        // Perform UPDATE and verify with GET
        new OperationWithAction(OperationCode.PUT, 1, OpFlags.USE_OLDCONN | OpFlags.USE_NEWVAL, 8),
        // For second client check that key2, key4, key6, key8 are accessible but others are not
        new OperationWithAction(OperationCode.GET, 2, OpFlags.USE_OLDCONN | OpFlags.USE_NEWVAL,
            new int[] {1, 3, 5, 7}),
        new OperationWithAction(OperationCode.GET, 2,
            OpFlags.USE_OLDCONN | OpFlags.USE_NEWVAL | OpFlags.CHECK_NOKEY | OpFlags.CHECK_NOTAUTHZ,
            new int[] {0, 2, 4, 6}),
        // For third client check that key3, key6 are accessible but others are not
        new OperationWithAction(OperationCode.GET, 3, OpFlags.USE_OLDCONN | OpFlags.USE_NEWVAL,
            new int[] {2, 5}),
        new OperationWithAction(OperationCode.GET, 3,
            OpFlags.USE_OLDCONN | OpFlags.USE_NEWVAL | OpFlags.CHECK_NOKEY | OpFlags.CHECK_NOTAUTHZ,
            new int[] {0, 1, 3, 4, 6, 7}),

        OperationWithAction.OPBLOCK_END,

        // Perform UPDATE and verify with GET_ALL
        new OperationWithAction(OperationCode.PUT, 1, OpFlags.USE_OLDCONN | OpFlags.USE_NEWVAL, 8),
        // For second client check that key2, key4, key6, key8 are accessible but others are not;
        // getAll test in doOp uses a combination of local entries and remote fetches
        new OperationWithAction(OperationCode.GET, 2,
            OpFlags.USE_OLDCONN | OpFlags.USE_NEWVAL | OpFlags.USE_ALL_KEYS,
            new int[] {1, 3, 5, 7}),
        new OperationWithAction(OperationCode.GET, 2,
            OpFlags.USE_OLDCONN | OpFlags.USE_NEWVAL | OpFlags.USE_ALL_KEYS | OpFlags.CHECK_NOKEY
                | OpFlags.CHECK_FAIL,
            new int[] {0, 2, 4, 6}),
        // For third client check that key3, key6 are accessible but others are not
        new OperationWithAction(OperationCode.GET, 3,
            OpFlags.USE_OLDCONN | OpFlags.USE_NEWVAL | OpFlags.USE_ALL_KEYS, new int[] {2, 5}),
        new OperationWithAction(OperationCode.GET, 3,
            OpFlags.USE_OLDCONN | OpFlags.USE_NEWVAL | OpFlags.USE_ALL_KEYS | OpFlags.CHECK_NOKEY
                | OpFlags.CHECK_FAIL,
            new int[] {0, 1, 3, 4, 6, 7}),

        // locally destroy the KEYS to also test create after failover
        new OperationWithAction(OperationCode.DESTROY, 1, OpFlags.USE_OLDCONN | OpFlags.LOCAL_OP,
            8),

        OperationWithAction.OPBLOCK_END,

        // Perform PUTALL and verify with GET
        new OperationWithAction(OperationCode.PUTALL, 1, OpFlags.USE_OLDCONN | OpFlags.USE_NEWVAL,
            8),
        // For second client check that key2, key4, key6, key8 are accessible but others are not
        new OperationWithAction(OperationCode.GET, 2, OpFlags.USE_OLDCONN | OpFlags.USE_NEWVAL,
            new int[] {1, 3, 5, 7}),
        new OperationWithAction(OperationCode.GET, 2,
            OpFlags.USE_OLDCONN | OpFlags.USE_NEWVAL | OpFlags.CHECK_NOKEY | OpFlags.CHECK_NOTAUTHZ,
            new int[] {0, 2, 4, 6}),
        // For third client check that key3, key6 are accessible but others are not
        new OperationWithAction(OperationCode.GET, 3, OpFlags.USE_OLDCONN | OpFlags.USE_NEWVAL,
            new int[] {2, 5}),
        new OperationWithAction(OperationCode.GET, 3,
            OpFlags.USE_OLDCONN | OpFlags.USE_NEWVAL | OpFlags.CHECK_NOKEY | OpFlags.CHECK_NOTAUTHZ,
            new int[] {0, 1, 3, 4, 6, 7}),

        OperationWithAction.OPBLOCK_END,

        // Test UPDATE and verify with a QUERY
        new OperationWithAction(OperationCode.PUT, 1, OpFlags.USE_OLDCONN, 8),
        // For second client check that key2, key4, key6, key8 are accessible but others are not
        new OperationWithAction(OperationCode.QUERY, 2, OpFlags.USE_OLDCONN,
            new int[] {1, 3, 5, 7}),
        new OperationWithAction(OperationCode.QUERY, 2, OpFlags.USE_OLDCONN | OpFlags.CHECK_FAIL,
            new int[] {0, 2, 4, 6}),
        // For third client check that key3, key6 are accessible but others are not
        new OperationWithAction(OperationCode.QUERY, 3, OpFlags.USE_OLDCONN, new int[] {2, 5}),
        new OperationWithAction(OperationCode.QUERY, 3, OpFlags.USE_OLDCONN | OpFlags.CHECK_FAIL,
            new int[] {0, 1, 3, 4, 6, 7}),

        OperationWithAction.OPBLOCK_END,

        // Test UPDATE and verify with a EXECUTE_CQ initial results
        new OperationWithAction(OperationCode.PUT, 1, OpFlags.USE_OLDCONN | OpFlags.USE_NEWVAL, 8),
        // For second client check that key2, key4, key6, key8 are accessible but others are not
        new OperationWithAction(OperationCode.EXECUTE_CQ, 2,
            OpFlags.USE_OLDCONN | OpFlags.USE_NEWVAL, new int[] {1, 3, 5, 7}),
        new OperationWithAction(OperationCode.CLOSE_CQ, 2, OpFlags.USE_OLDCONN, 1),
        new OperationWithAction(OperationCode.EXECUTE_CQ, 2,
            OpFlags.USE_OLDCONN | OpFlags.USE_NEWVAL | OpFlags.CHECK_FAIL, new int[] {0, 2, 4, 6}),
        new OperationWithAction(OperationCode.CLOSE_CQ, 2, OpFlags.USE_OLDCONN, 1),
        // For third client check that key3, key6 are accessible but others are not
        new OperationWithAction(OperationCode.EXECUTE_CQ, 3,
            OpFlags.USE_OLDCONN | OpFlags.USE_NEWVAL, new int[] {2, 5}),
        new OperationWithAction(OperationCode.CLOSE_CQ, 3, OpFlags.USE_OLDCONN, 1),
        new OperationWithAction(OperationCode.EXECUTE_CQ, 3,
            OpFlags.USE_OLDCONN | OpFlags.USE_NEWVAL | OpFlags.CHECK_FAIL,
            new int[] {0, 1, 3, 4, 6, 7}),
        new OperationWithAction(OperationCode.CLOSE_CQ, 3, OpFlags.USE_OLDCONN, 1),

        OperationWithAction.OPBLOCK_END};
  }

  private Properties buildProperties(final String authenticator, final Properties extraProps,
      final String preAccessor, final String postAccessor) {
    Properties authProps = new Properties();
    if (authenticator != null) {
      authProps.setProperty(ConfigurationProperties.SECURITY_CLIENT_AUTHENTICATOR, authenticator);
    }
    if (preAccessor != null) {
      authProps.setProperty(ConfigurationProperties.SECURITY_CLIENT_ACCESSOR, preAccessor);
    }
    if (postAccessor != null) {
      authProps.setProperty(ConfigurationProperties.SECURITY_CLIENT_ACCESSOR_PP, postAccessor);
    }
    if (extraProps != null) {
      authProps.putAll(extraProps);
    }
    return authProps;
  }

  private static class MyInstantiator extends Instantiator {

    public MyInstantiator() {
      this(ObjectWithAuthz.class, ObjectWithAuthz.CLASSID);
    }

    public MyInstantiator(final Class clazz, final int classId) {
      super(clazz, classId);
    }

    @Override
    public DataSerializable newInstance() {
      return new ObjectWithAuthz();
    }
  }

  private static class TestPostCredentialGenerator implements TestCredentialGenerator {

    public TestPostCredentialGenerator() {}

    @Override
    public Properties getAllowedCredentials(final OperationCode[] opCodes,
        final String[] regionNames, final int[] keyIndices, final int num) {
      int userIndex = 1;
      byte role = DummyAuthzCredentialGenerator.getRequiredRole(opCodes);
      if (role == DummyAuthzCredentialGenerator.READER_ROLE) {
        userIndex = keyIndices[0] + 1;
      }
      Properties props = new Properties();
      props.setProperty(UserPasswordAuthInit.USER_NAME, "user" + userIndex);
      props.setProperty(UserPasswordAuthInit.PASSWORD, "user" + userIndex);
      return props;
    }

    @Override
    public Properties getDisallowedCredentials(final OperationCode[] opCodes,
        final String[] regionNames, final int[] keyIndices, final int num) {
      int userIndex = 0;
      for (int index = 0; index < keyIndices.length; ++index) {
        if (keyIndices[index] != index) {
          userIndex = index + 1;
          break;
        }
      }
      Properties props = new Properties();
      props.setProperty(UserPasswordAuthInit.USER_NAME, "gemfire" + userIndex);
      props.setProperty(UserPasswordAuthInit.PASSWORD, "gemfire" + userIndex);
      return props;
    }

    @Override
    public CredentialGenerator getCredentialGenerator() {
      return null;
    }
  }
}
