/*
 * 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.cache.query.security;


import static org.apache.geode.distributed.ConfigurationProperties.SECURITY_MANAGER;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamException;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.charset.Charset;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.DoubleAccumulator;
import java.util.concurrent.atomic.DoubleAdder;
import java.util.concurrent.atomic.LongAccumulator;
import java.util.concurrent.atomic.LongAdder;

import org.apache.shiro.SecurityUtils;
import org.junit.Before;
import org.junit.Test;
import org.junit.experimental.categories.Category;

import org.apache.geode.cache.Cache;
import org.apache.geode.cache.EntryEvent;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.query.internal.QRegion;
import org.apache.geode.cache.query.internal.index.DummyQRegion;
import org.apache.geode.cache.wan.GatewayQueueEvent;
import org.apache.geode.distributed.DistributedSystem;
import org.apache.geode.examples.SimpleSecurityManager;
import org.apache.geode.internal.cache.EntrySnapshot;
import org.apache.geode.internal.cache.InternalCache;
import org.apache.geode.internal.cache.LocalRegion;
import org.apache.geode.internal.cache.NonTXEntry;
import org.apache.geode.internal.cache.PartitionedRegion;
import org.apache.geode.internal.cache.PartitionedRegionDataStore;
import org.apache.geode.internal.cache.ha.HAContainerMap;
import org.apache.geode.internal.security.IntegratedSecurityService;
import org.apache.geode.internal.security.LegacySecurityService;
import org.apache.geode.internal.security.SecurityService;
import org.apache.geode.security.NotAuthorizedException;
import org.apache.geode.security.ResourcePermission;
import org.apache.geode.test.junit.categories.SecurityTest;

@Category(SecurityTest.class)
public class RestrictedMethodAuthorizerTest {
  private Cache mockCache;
  private SecurityService mockSecurityService;
  private RestrictedMethodAuthorizer methodAuthorizer;

  @Before
  public void setUp() {
    mockCache = mock(InternalCache.class);
    mockSecurityService = mock(SecurityService.class);
    when(((InternalCache) mockCache).getSecurityService()).thenReturn(mockSecurityService);

    methodAuthorizer = new RestrictedMethodAuthorizer(mockCache);

    // Make sure the shiro security-manager is null to avoid polluting tests.
    SecurityUtils.setSecurityManager(null);
  }

  @Test
  public void constructorShouldThrowExceptionWhenCacheIsNull() {
    assertThatThrownBy(() -> new RestrictedMethodAuthorizer(null))
        .isInstanceOf(NullPointerException.class)
        .hasMessage("Cache should be provided to configure the authorizer.");
  }

  @Test
  public void constructorShouldSetTheConfiguredSecurityServiceForGeodeCreatedCacheInstances() {
    InternalCache cache = mock(InternalCache.class);
    SecurityService securityService = mock(SecurityService.class);
    when(cache.getSecurityService()).thenReturn(securityService);

    RestrictedMethodAuthorizer defaultAuthorizer = new RestrictedMethodAuthorizer(cache);
    verify(cache).getSecurityService();
    assertThat(defaultAuthorizer.securityService).isSameAs(securityService);
  }

  @Test
  public void constructorShouldThrowExceptionForNonGeodeCreatedCacheInstancesWhenDistributedSystemIsNull() {
    Cache cache = mock(Cache.class);

    assertThatThrownBy(() -> new RestrictedMethodAuthorizer(cache))
        .isInstanceOf(NullPointerException.class)
        .hasMessage(
            "Distributed system properties should be provided to configure the authorizer.");
  }

  @Test
  public void constructorShouldSetTheLegacySecurityServiceForNonGeodeCreatedCacheInstancesWithDefaultProperties() {
    Cache mockCache = mock(Cache.class);
    DistributedSystem mockDistributedSystem = mock(DistributedSystem.class);
    when(mockCache.getDistributedSystem()).thenReturn(mockDistributedSystem);

    RestrictedMethodAuthorizer authorizerWithLegacyService =
        new RestrictedMethodAuthorizer(mockCache);
    verify(mockDistributedSystem).getSecurityProperties();
    assertThat(authorizerWithLegacyService.securityService)
        .isInstanceOf(LegacySecurityService.class);
  }

  @Test
  public void constructorShouldSetTheIntegratedSecurityServiceForNonGeodeCreatedCacheInstancesWithNonDefaultProperties() {
    Cache mockCache = mock(Cache.class);
    Properties securityProperties = new Properties();
    securityProperties.setProperty(SECURITY_MANAGER, SimpleSecurityManager.class.getName());
    DistributedSystem mockDistributedSystem = mock(DistributedSystem.class);
    when(mockCache.getDistributedSystem()).thenReturn(mockDistributedSystem);
    when(mockDistributedSystem.getSecurityProperties()).thenReturn(securityProperties);

    RestrictedMethodAuthorizer authorizerWithIntegratedService =
        new RestrictedMethodAuthorizer(mockCache);
    verify(mockDistributedSystem).getSecurityProperties();
    assertThat(authorizerWithIntegratedService.securityService)
        .isInstanceOf(IntegratedSecurityService.class);
    assertThat(authorizerWithIntegratedService.securityService.getSecurityManager())
        .isInstanceOf(SimpleSecurityManager.class);
  }

  @Test
  public void internalStructuresShouldBeAccessibleBuNotModifiable() {
    assertThatThrownBy(() -> methodAuthorizer.getForbiddenMethods().remove("getClass"))
        .isInstanceOf(UnsupportedOperationException.class);

    assertThatThrownBy(() -> methodAuthorizer.getAllowedMethodsPerClass().remove("compareTo"))
        .isInstanceOf(UnsupportedOperationException.class);
    assertThatThrownBy(() -> methodAuthorizer.getAllowedMethodsPerClass().get("compareTo")
        .remove(Object.class)).isInstanceOf(UnsupportedOperationException.class);

    assertThatThrownBy(() -> methodAuthorizer.getAllowedGeodeMethodsPerClass().remove("get"))
        .isInstanceOf(UnsupportedOperationException.class);
    assertThatThrownBy(() -> methodAuthorizer.getAllowedGeodeMethodsPerClass().get("get")
        .remove(Region.class)).isInstanceOf(UnsupportedOperationException.class);
  }

  @Test
  public void verifyAuthorizersUsesDefaultForbiddenList() {
    RestrictedMethodAuthorizer authorizer1 = new RestrictedMethodAuthorizer(mockCache);
    RestrictedMethodAuthorizer authorizer2 = new RestrictedMethodAuthorizer(mockCache);

    assertThat(authorizer1.getForbiddenMethods()).isSameAs(authorizer2.getForbiddenMethods());
    assertThat(authorizer1.getForbiddenMethods())
        .isSameAs(RestrictedMethodAuthorizer.FORBIDDEN_METHODS);
    assertThat(authorizer2.getForbiddenMethods())
        .isSameAs(RestrictedMethodAuthorizer.FORBIDDEN_METHODS);
  }

  @Test
  public void verifyAuthorizersUsesDefaultAllowedList() {
    RestrictedMethodAuthorizer authorizer1 = new RestrictedMethodAuthorizer(mockCache);
    RestrictedMethodAuthorizer authorizer2 = new RestrictedMethodAuthorizer(mockCache);

    assertThat(authorizer1.getAllowedMethodsPerClass())
        .isSameAs(authorizer2.getAllowedMethodsPerClass());
    assertThat(authorizer1.getAllowedMethodsPerClass())
        .isSameAs(RestrictedMethodAuthorizer.DEFAULT_ALLOWED_METHODS);
    assertThat(authorizer2.getAllowedMethodsPerClass())
        .isSameAs(RestrictedMethodAuthorizer.DEFAULT_ALLOWED_METHODS);
  }

  @Test
  public void verifyAuthorizersUsesGeodeAllowedList() {
    RestrictedMethodAuthorizer authorizer1 = new RestrictedMethodAuthorizer(mockCache);
    RestrictedMethodAuthorizer authorizer2 = new RestrictedMethodAuthorizer(mockCache);

    assertThat(authorizer1.getAllowedGeodeMethodsPerClass())
        .isSameAs(authorizer2.getAllowedGeodeMethodsPerClass());
    assertThat(authorizer1.getAllowedGeodeMethodsPerClass())
        .isSameAs(RestrictedMethodAuthorizer.GEODE_ALLOWED_METHODS);
    assertThat(authorizer2.getAllowedGeodeMethodsPerClass())
        .isSameAs(RestrictedMethodAuthorizer.GEODE_ALLOWED_METHODS);
  }

  @SuppressWarnings("unchecked")
  private void verifyObjectMethods(Class type, Object object) {
    try {
      Method toStringMethod = type.getMethod("toString");
      Method equalsMethod = type.getMethod("equals", Object.class);

      assertThat(methodAuthorizer.authorize(toStringMethod, object)).isTrue();
      assertThat(methodAuthorizer.authorize(equalsMethod, object)).isTrue();
    } catch (NoSuchMethodException noSuchMethodException) {
      throw new RuntimeException(noSuchMethodException);
    }
  }

  @SuppressWarnings("unchecked")
  private void verifyComparableMethods(Class type, Object object) {
    try {
      Method compareToMethod = type.getMethod("compareTo", Object.class);

      assertThat(methodAuthorizer.authorize(compareToMethod, object)).isTrue();
    } catch (NoSuchMethodException noSuchMethodException) {
      throw new RuntimeException(noSuchMethodException);
    }
  }

  @Test
  public void authorizeShouldReturnFalseForNotAllowedMethods() throws Exception {
    TestBean testBean = new TestBean();
    List<Method> dangerousMethods = new ArrayList<>();
    dangerousMethods.add(TestBean.class.getMethod("getClass"));
    dangerousMethods.add(TestBean.class.getMethod("readResolve"));
    dangerousMethods.add(TestBean.class.getMethod("readObjectNoData"));
    dangerousMethods.add(TestBean.class.getMethod("readObject", ObjectInputStream.class));
    dangerousMethods.add(TestBean.class.getMethod("writeReplace"));
    dangerousMethods.add(TestBean.class.getMethod("writeObject", ObjectOutputStream.class));

    dangerousMethods
        .forEach(method -> assertThat(methodAuthorizer.authorize(method, testBean)).isFalse());
  }

  @Test
  public void authorizeShouldReturnTrueForAllowedMethodsOnAnyObjectInstance()
      throws NoSuchMethodException {
    Method toStringMethod = Object.class.getMethod("toString");
    Method equalsMethod = Object.class.getMethod("equals", Object.class);
    Method compareToMethod = Comparable.class.getMethod("compareTo", Object.class);

    assertThat(methodAuthorizer.authorize(toStringMethod, new Object())).isTrue();
    assertThat(methodAuthorizer.authorize(equalsMethod, new Object())).isTrue();
    assertThat(methodAuthorizer.authorize(compareToMethod, new Object())).isTrue();
  }

  @Test
  public void authorizeShouldReturnTrueForAllowedMethodsOnBooleanInstances() throws Exception {
    Method booleanValueMethod = Boolean.class.getMethod("booleanValue");
    assertThat(methodAuthorizer.authorize(booleanValueMethod, Boolean.TRUE)).isTrue();

    verifyObjectMethods(Boolean.class, Boolean.TRUE);
    verifyComparableMethods(Boolean.class, Boolean.TRUE);
  }

  @Test
  public void authorizeShouldReturnTrueForAllowedMethodsOnNumberInstances() throws Exception {
    List<Number> instances = new ArrayList<>();
    instances.add(new AtomicInteger(0));
    instances.add(new AtomicLong(0));
    instances.add(new BigDecimal("0"));
    instances.add(new BigInteger("0"));
    instances.add(new Byte("0"));
    instances.add(new Double("0d"));
    instances.add(new DoubleAccumulator(Double::sum, 0L));
    instances.add(new DoubleAdder());
    instances.add(new Float("1f"));
    instances.add(new Integer("1"));
    instances.add(new Long("1"));
    instances.add(new LongAccumulator(Long::sum, 0L));
    instances.add(new LongAdder());
    instances.add(new Short("1"));

    Method byteValueMethod = Number.class.getMethod("byteValue");
    Method doubleValueMethod = Number.class.getMethod("doubleValue");
    Method intValueMethod = Number.class.getMethod("intValue");
    Method floatValueMethod = Number.class.getMethod("longValue");
    Method longValueMethod = Number.class.getMethod("floatValue");
    Method shortValueMethod = Number.class.getMethod("shortValue");

    instances.forEach((number) -> {
      assertThat(methodAuthorizer.authorize(byteValueMethod, number)).isTrue();
      assertThat(methodAuthorizer.authorize(doubleValueMethod, number)).isTrue();
      assertThat(methodAuthorizer.authorize(intValueMethod, number)).isTrue();
      assertThat(methodAuthorizer.authorize(floatValueMethod, number)).isTrue();
      assertThat(methodAuthorizer.authorize(longValueMethod, number)).isTrue();
      assertThat(methodAuthorizer.authorize(shortValueMethod, number)).isTrue();
      verifyObjectMethods(number.getClass(), number);
    });
  }

  @Test
  public void authorizeShouldReturnTrueForAllowedMethodsOnDateInstances() throws Exception {
    List<Date> instances = new ArrayList<>();
    instances.add(new Date(0));
    instances.add(new java.sql.Date(0));
    instances.add(new Time(0));
    instances.add(new Timestamp(0));

    Method afterMethod = Date.class.getMethod("after", Date.class);
    Method beforeMethod = Date.class.getMethod("before", Date.class);
    Method getTimeMethod = Date.class.getMethod("getTime");

    instances.forEach((date) -> {
      assertThat(methodAuthorizer.authorize(afterMethod, date)).isTrue();
      assertThat(methodAuthorizer.authorize(beforeMethod, date)).isTrue();
      assertThat(methodAuthorizer.authorize(getTimeMethod, date)).isTrue();

      verifyObjectMethods(date.getClass(), date);
      verifyComparableMethods(date.getClass(), date);
    });
  }

  @Test
  public void authorizeShouldReturnTrueForAllowedMethodsOnTimestampInstances() throws Exception {
    Timestamp timestamp = new Timestamp(0);
    Method afterMethod = Timestamp.class.getMethod("after", Date.class);
    Method beforeMethod = Timestamp.class.getMethod("before", Date.class);
    Method getNanosMethod = Timestamp.class.getMethod("getNanos");
    Method getTimeMethod = Timestamp.class.getMethod("getTime");

    assertThat(methodAuthorizer.authorize(afterMethod, timestamp)).isTrue();
    assertThat(methodAuthorizer.authorize(beforeMethod, timestamp)).isTrue();
    assertThat(methodAuthorizer.authorize(getNanosMethod, timestamp)).isTrue();
    assertThat(methodAuthorizer.authorize(getTimeMethod, timestamp)).isTrue();

    verifyObjectMethods(Timestamp.class, timestamp);
    verifyComparableMethods(Timestamp.class, timestamp);
  }

  @Test
  public void authorizeShouldReturnTrueForAllowedMethodsOnStringInstances() throws Exception {
    String string = "";
    List<Method> stringMethods = new ArrayList<>();
    stringMethods.add(String.class.getMethod("charAt", int.class));
    stringMethods.add(String.class.getMethod("codePointAt", int.class));
    stringMethods.add(String.class.getMethod("codePointBefore", int.class));
    stringMethods.add(String.class.getMethod("codePointCount", int.class, int.class));
    stringMethods.add(String.class.getMethod("compareToIgnoreCase", String.class));
    stringMethods.add(String.class.getMethod("concat", String.class));
    stringMethods.add(String.class.getMethod("contains", CharSequence.class));
    stringMethods.add(String.class.getMethod("contentEquals", CharSequence.class));
    stringMethods.add(String.class.getMethod("contentEquals", StringBuffer.class));
    stringMethods.add(String.class.getMethod("endsWith", String.class));
    stringMethods.add(String.class.getMethod("equalsIgnoreCase", String.class));
    stringMethods.add(String.class.getMethod("getBytes"));
    stringMethods.add(String.class.getMethod("getBytes", Charset.class));
    stringMethods.add(String.class.getMethod("hashCode"));
    stringMethods.add(String.class.getMethod("indexOf", int.class));
    stringMethods.add(String.class.getMethod("indexOf", String.class));
    stringMethods.add(String.class.getMethod("indexOf", String.class, int.class));
    stringMethods.add(String.class.getMethod("intern"));
    stringMethods.add(String.class.getMethod("isEmpty"));
    stringMethods.add(String.class.getMethod("lastIndexOf", int.class));
    stringMethods.add(String.class.getMethod("lastIndexOf", int.class, int.class));
    stringMethods.add(String.class.getMethod("lastIndexOf", String.class));
    stringMethods.add(String.class.getMethod("lastIndexOf", String.class, int.class));
    stringMethods.add(String.class.getMethod("length"));
    stringMethods.add(String.class.getMethod("matches", String.class));
    stringMethods.add(String.class.getMethod("offsetByCodePoints", int.class, int.class));
    stringMethods.add(String.class.getMethod("regionMatches", boolean.class, int.class,
        String.class, int.class, int.class));
    stringMethods.add(
        String.class.getMethod("regionMatches", int.class, String.class, int.class, int.class));
    stringMethods.add(String.class.getMethod("replace", char.class, char.class));
    stringMethods.add(String.class.getMethod("replace", CharSequence.class, CharSequence.class));
    stringMethods.add(String.class.getMethod("replaceAll", String.class, String.class));
    stringMethods.add(String.class.getMethod("replaceFirst", String.class, String.class));
    stringMethods.add(String.class.getMethod("split", String.class));
    stringMethods.add(String.class.getMethod("split", String.class, int.class));
    stringMethods.add(String.class.getMethod("startsWith", String.class));
    stringMethods.add(String.class.getMethod("startsWith", String.class, int.class));
    stringMethods.add(String.class.getMethod("substring", int.class));
    stringMethods.add(String.class.getMethod("substring", int.class, int.class));
    stringMethods.add(String.class.getMethod("toCharArray"));
    stringMethods.add(String.class.getMethod("toLowerCase"));
    stringMethods.add(String.class.getMethod("toUpperCase"));
    stringMethods.add(String.class.getMethod("trim"));

    stringMethods
        .forEach(method -> assertThat(methodAuthorizer.authorize(method, string)).isTrue());
    verifyObjectMethods(String.class, string);
    verifyComparableMethods(String.class, string);
  }

  @Test
  public void authorizeShouldReturnTrueForAllowedMethodsOnMapEntryInstances() throws Exception {
    List<Map.Entry> instances = new ArrayList<>();
    instances.add(mock(Map.Entry.class));
    instances.add(mock(NonTXEntry.class));
    instances.add(mock(EntrySnapshot.class));
    instances.add(mock(Region.Entry.class));
    Method getKeyMethod = Map.Entry.class.getMethod("getKey");
    Method getValueMethod = Map.Entry.class.getMethod("getValue");

    instances.forEach((mapEntry) -> {
      assertThat(methodAuthorizer.authorize(getKeyMethod, mapEntry)).isTrue();
      assertThat(methodAuthorizer.authorize(getValueMethod, mapEntry)).isTrue();
      verifyObjectMethods(mapEntry.getClass(), mapEntry);
    });
  }

  @Test
  public void authorizeShouldReturnTrueForAllowedMethodsOnMapInstances() throws Exception {
    Map mapInstance = mock(Map.class);
    List<Method> mapMethods = new ArrayList<>();
    mapMethods.add(Map.class.getMethod("containsKey", Object.class));
    mapMethods.add(Map.class.getMethod("entrySet"));
    mapMethods.add(Map.class.getMethod("get", Object.class));
    mapMethods.add(Map.class.getMethod("keySet"));
    mapMethods.add(Map.class.getMethod("values"));

    mapMethods
        .forEach(method -> assertThat(methodAuthorizer.authorize(method, mapInstance)).isTrue());
    verifyObjectMethods(mapInstance.getClass(), mapInstance);
  }

  @Test
  public void authorizeShouldReturnTrueForAllowedMethodsOnQRegionInstances() throws Exception {
    QRegion qRegionInstance = mock(QRegion.class);
    DummyQRegion dummyQRegionInstance = mock(DummyQRegion.class);

    List<Method> methods = new ArrayList<>();
    methods.add(Map.class.getMethod("containsKey", Object.class));
    methods.add(Map.class.getMethod("entrySet"));
    methods.add(Map.class.getMethod("get", Object.class));
    methods.add(Map.class.getMethod("keySet"));
    methods.add(Map.class.getMethod("values"));
    methods.add(QRegion.class.getMethod("getEntries"));
    methods.add(QRegion.class.getMethod("getValues"));

    methods.forEach(method -> {
      assertThat(methodAuthorizer.authorize(method, qRegionInstance)).isTrue();
      assertThat(methodAuthorizer.authorize(method, dummyQRegionInstance)).isTrue();
    });

    verifyObjectMethods(qRegionInstance.getClass(), qRegionInstance);
    verifyObjectMethods(dummyQRegionInstance.getClass(), dummyQRegionInstance);
  }

  @Test
  public void authorizeShouldReturnTrueForMapMethodsOnRegionInstancesWheneverTheSecurityServiceAllowsOperationsOnTheRegion()
      throws Exception {
    Region region = mock(Region.class);
    Region localRegion = mock(LocalRegion.class);
    PartitionedRegion partitionedRegion = mock(PartitionedRegion.class);

    List<Method> methods = new ArrayList<>();
    methods.add(Map.class.getMethod("containsKey", Object.class));
    methods.add(Map.class.getMethod("entrySet"));
    methods.add(Map.class.getMethod("get", Object.class));
    methods.add(Map.class.getMethod("keySet"));
    methods.add(Map.class.getMethod("values"));

    methods.forEach(method -> {
      assertThat(methodAuthorizer.authorize(method, region)).isTrue();
      assertThat(methodAuthorizer.authorize(method, localRegion)).isTrue();
      assertThat(methodAuthorizer.authorize(method, partitionedRegion)).isTrue();
    });

    verifyObjectMethods(region.getClass(), region);
    verifyObjectMethods(localRegion.getClass(), localRegion);
    verifyObjectMethods(partitionedRegion.getClass(), partitionedRegion);
  }

  @Test
  public void authorizeShouldReturnFalseWheneverTheSecurityServiceDoesNotAllowOperationsOnTheRegion()
      throws Exception {
    doThrow(new NotAuthorizedException("Mock Exception")).when(mockSecurityService).authorize(
        ResourcePermission.Resource.DATA, ResourcePermission.Operation.READ, "testRegion");
    Region region = mock(Region.class);
    when(region.getName()).thenReturn("testRegion");
    PartitionedRegion partitionedRegion = mock(PartitionedRegion.class);
    when(partitionedRegion.getName()).thenReturn("testRegion");

    List<Method> methods = new ArrayList<>();
    methods.add(Map.class.getMethod("containsKey", Object.class));
    methods.add(Map.class.getMethod("entrySet"));
    methods.add(Map.class.getMethod("get", Object.class));
    methods.add(Map.class.getMethod("keySet"));
    methods.add(Map.class.getMethod("values"));

    methods.forEach(method -> {
      assertThat(methodAuthorizer.authorize(method, region)).isFalse();
      assertThat(methodAuthorizer.authorize(method, partitionedRegion)).isFalse();
    });
  }

  @Test
  public void isAllowedGeodeMethodShouldReturnFalseForNonGeodeObjectInstances() throws Exception {
    Map mapInstance = mock(Map.class);
    List<Method> mapMethods = new ArrayList<>();
    mapMethods.add(Map.class.getMethod("containsKey", Object.class));
    mapMethods.add(Map.class.getMethod("entrySet"));
    mapMethods.add(Map.class.getMethod("get", Object.class));
    mapMethods.add(Map.class.getMethod("keySet"));
    mapMethods.add(Map.class.getMethod("values"));

    Map.Entry mapEntryInstance = mock(Map.Entry.class);
    List<Method> mapEntryMethods = new ArrayList<>();
    mapEntryMethods.add(Map.Entry.class.getMethod("getKey"));
    mapEntryMethods.add(Map.Entry.class.getMethod("getValue"));

    mapMethods.forEach(
        method -> assertThat(methodAuthorizer.isAllowedGeodeMethod(method, mapInstance)).isFalse());
    mapEntryMethods.forEach(
        method -> assertThat(methodAuthorizer.isAllowedGeodeMethod(method, mapEntryInstance))
            .isFalse());
  }

  @Test
  public void isAllowedGeodeMethodShouldReturnFalseForGeodeObjectsThatAreNotInstanceOfRegionAndRegionEntry()
      throws Exception {
    EntryEvent entryEvent = mock(EntryEvent.class);
    HAContainerMap haContainerMap = mock(HAContainerMap.class);
    GatewayQueueEvent gatewayQueueEvent = mock(GatewayQueueEvent.class);
    PartitionedRegionDataStore partitionedRegionDataStore = mock(PartitionedRegionDataStore.class);

    List<Method> queueRegionMethods = new ArrayList<>();
    queueRegionMethods.add(QRegion.class.getMethod("getEntries"));
    queueRegionMethods.add(QRegion.class.getMethod("getValues"));

    List<Method> regionEntryMethods = new ArrayList<>();
    regionEntryMethods.add(Region.Entry.class.getMethod("getKey"));
    regionEntryMethods.add(Region.Entry.class.getMethod("getValue"));

    List<Method> regionMethods = new ArrayList<>();
    regionMethods.add(Region.class.getMethod("containsKey", Object.class));
    regionMethods.add(Region.class.getMethod("entrySet"));
    regionMethods.add(Region.class.getMethod("get", Object.class));
    regionMethods.add(Region.class.getMethod("keySet"));
    regionMethods.add(Region.class.getMethod("values"));

    regionEntryMethods.forEach(method -> {
      assertThat(methodAuthorizer.isAllowedGeodeMethod(method, entryEvent)).isFalse();
      assertThat(methodAuthorizer.isAllowedGeodeMethod(method, gatewayQueueEvent)).isFalse();
    });

    regionMethods.forEach(method -> {
      assertThat(methodAuthorizer.isAllowedGeodeMethod(method, haContainerMap)).isFalse();
      assertThat(methodAuthorizer.isAllowedGeodeMethod(method, partitionedRegionDataStore))
          .isFalse();
    });

    queueRegionMethods.forEach(method -> {
      assertThat(methodAuthorizer.isAllowedGeodeMethod(method, haContainerMap)).isFalse();
      assertThat(methodAuthorizer.isAllowedGeodeMethod(method, partitionedRegionDataStore))
          .isFalse();
    });
  }

  @Test
  public void isAllowedGeodeMethodShouldReturnTrueForRegionEntryMethods() throws Exception {
    List<Region.Entry> regionEntryInstances = new ArrayList<>();
    regionEntryInstances.add(mock(NonTXEntry.class));
    regionEntryInstances.add(mock(Region.Entry.class));
    regionEntryInstances.add(mock(EntrySnapshot.class));

    Method getKeyMethod = Region.Entry.class.getMethod("getKey");
    Method getValueMethod = Region.Entry.class.getMethod("getValue");
    Method toStringMethod = Object.class.getMethod("toString");
    Method equalsMethod = Object.class.getMethod("equals", Object.class);

    regionEntryInstances.forEach((regionEntry) -> {
      assertThat(methodAuthorizer.isAllowedGeodeMethod(getKeyMethod, regionEntry)).isTrue();
      assertThat(methodAuthorizer.isAllowedGeodeMethod(getValueMethod, regionEntry)).isTrue();
      assertThat(methodAuthorizer.isAllowedGeodeMethod(equalsMethod, regionEntry)).isTrue();
      assertThat(methodAuthorizer.isAllowedGeodeMethod(toStringMethod, regionEntry)).isTrue();
    });
  }

  @Test
  public void isAllowedGeodeMethodShouldReturnTrueForRegionMethodsOnRegionInstancesWheneverTheSecurityServiceAllowsOperationsOnTheRegion()
      throws Exception {
    Region region = mock(Region.class);
    LocalRegion localRegion = mock(LocalRegion.class);
    PartitionedRegion partitionedRegion = mock(PartitionedRegion.class);

    List<Method> regionMethods = new ArrayList<>();
    regionMethods.add(Region.class.getMethod("containsKey", Object.class));
    regionMethods.add(Region.class.getMethod("entrySet"));
    regionMethods.add(Region.class.getMethod("get", Object.class));
    regionMethods.add(Region.class.getMethod("keySet"));
    regionMethods.add(Region.class.getMethod("values"));
    regionMethods.add(Object.class.getMethod("toString"));
    regionMethods.add(Object.class.getMethod("equals", Object.class));

    regionMethods.forEach(method -> {
      assertThat(methodAuthorizer.isAllowedGeodeMethod(method, region)).isTrue();
      assertThat(methodAuthorizer.isAllowedGeodeMethod(method, localRegion)).isTrue();
      assertThat(methodAuthorizer.isAllowedGeodeMethod(method, partitionedRegion)).isTrue();
    });
  }

  @Test
  public void isAllowedGeodeMethodShouldReturnFalseWheneverTheSecurityServiceDoesNotAllowOperationsOnTheRegion()
      throws Exception {
    doThrow(new NotAuthorizedException("Mock Exception")).when(mockSecurityService).authorize(
        ResourcePermission.Resource.DATA, ResourcePermission.Operation.READ, "testRegion");
    Region region = mock(Region.class);
    when(region.getName()).thenReturn("testRegion");
    LocalRegion localRegion = mock(LocalRegion.class);
    when(localRegion.getName()).thenReturn("testRegion");
    PartitionedRegion partitionedRegion = mock(PartitionedRegion.class);
    when(partitionedRegion.getName()).thenReturn("testRegion");

    List<Method> regionMethods = new ArrayList<>();
    regionMethods.add(Region.class.getMethod("containsKey", Object.class));
    regionMethods.add(Region.class.getMethod("entrySet"));
    regionMethods.add(Region.class.getMethod("get", Object.class));
    regionMethods.add(Region.class.getMethod("keySet"));
    regionMethods.add(Region.class.getMethod("values"));

    regionMethods.forEach(method -> {
      assertThat(methodAuthorizer.isAllowedGeodeMethod(method, region)).isFalse();
      assertThat(methodAuthorizer.isAllowedGeodeMethod(method, localRegion)).isFalse();
      assertThat(methodAuthorizer.isAllowedGeodeMethod(method, partitionedRegion)).isFalse();
    });
  }

  @Test
  public void isPermanentlyForbiddenMethodShouldReturnTrueForNonSafeMethods() throws Exception {
    TestBean testBean = new TestBean();
    List<Method> dangerousMethods = new ArrayList<>();
    dangerousMethods.add(TestBean.class.getMethod("getClass"));
    dangerousMethods.add(TestBean.class.getMethod("readResolve"));
    dangerousMethods.add(TestBean.class.getMethod("readObjectNoData"));
    dangerousMethods.add(TestBean.class.getMethod("readObject", ObjectInputStream.class));
    dangerousMethods.add(TestBean.class.getMethod("writeReplace"));
    dangerousMethods.add(TestBean.class.getMethod("writeObject", ObjectOutputStream.class));

    dangerousMethods.forEach(
        method -> assertThat(methodAuthorizer.isPermanentlyForbiddenMethod(method, testBean))
            .isTrue());
  }

  @SuppressWarnings("unused")
  private static class TestBean implements Serializable {
    public Object writeReplace() throws ObjectStreamException {
      return new TestBean();
    }

    public void writeObject(ObjectOutputStream stream) throws IOException {
      throw new IOException();
    }

    public Object readResolve() throws ObjectStreamException {
      return new TestBean();
    }

    public void readObjectNoData() throws ObjectStreamException {}

    public void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
      if (new Random().nextBoolean()) {
        throw new IOException();
      } else {
        throw new ClassNotFoundException();
      }
    }
  }
}
