blob: 5153ea00e335a313d825770555e67a83533a93c6 [file] [log] [blame]
/*
* 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.cache.query.security.RegExMethodAuthorizer.NULL_AUTHORIZER_MESSAGE;
import static org.apache.geode.cache.query.security.RegExMethodAuthorizer.NULL_CACHE_MESSAGE;
import static org.apache.geode.cache.query.security.RegExMethodAuthorizer.NULL_REGULAR_EXPRESSIONS_MESSAGE;
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.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.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
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.configuration.RegionConfig;
import org.apache.geode.internal.cache.BucketDump;
import org.apache.geode.internal.cache.InternalCache;
import org.apache.geode.internal.cache.LocalRegion;
import org.apache.geode.internal.cache.PartitionedRegion;
import org.apache.geode.internal.cache.ha.HAContainerMap;
import org.apache.geode.internal.cache.persistence.query.mock.Pair;
import org.apache.geode.internal.security.SecurityService;
import org.apache.geode.internal.util.Valuable;
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 RegExMethodAuthorizerTest {
private InternalCache mockCache;
private SecurityService mockSecurityService;
private RegExMethodAuthorizer geodeRegExMatchingMethodAuthorizer;
@Before
public void setUp() {
mockCache = mock(InternalCache.class);
mockSecurityService = mock(SecurityService.class);
when(mockCache.getSecurityService()).thenReturn(mockSecurityService);
geodeRegExMatchingMethodAuthorizer =
new RegExMethodAuthorizer(new RestrictedMethodAuthorizer(mockCache),
Collections.singleton("^org\\.apache\\.geode\\..*"));
}
@Test
public void constructorShouldThrowExceptionWhenCacheIsNull() {
assertThatThrownBy(() -> new RegExMethodAuthorizer((Cache) null, Collections.emptySet()))
.isInstanceOf(NullPointerException.class)
.hasMessage(NULL_CACHE_MESSAGE);
}
@Test
public void constructorShouldThrowExceptionWhenRestrictedMethodAuthorizerIsNull() {
assertThatThrownBy(
() -> new RegExMethodAuthorizer((RestrictedMethodAuthorizer) null, Collections.emptySet()))
.isInstanceOf(NullPointerException.class)
.hasMessage(NULL_AUTHORIZER_MESSAGE);
}
@Test
public void constructorShouldThrowExceptionWhenAllowedPatternsIsNull() {
assertThatThrownBy(() -> new RegExMethodAuthorizer(mockCache, null))
.isInstanceOf(NullPointerException.class)
.hasMessage(NULL_REGULAR_EXPRESSIONS_MESSAGE);
assertThatThrownBy(
() -> new RegExMethodAuthorizer(new RestrictedMethodAuthorizer(mockCache), null))
.isInstanceOf(NullPointerException.class)
.hasMessage(NULL_REGULAR_EXPRESSIONS_MESSAGE);
}
@Test
public void allowedPatternsShouldBeAccessibleBuNotModifiable() {
assertThatThrownBy(() -> geodeRegExMatchingMethodAuthorizer.getAllowedPatterns().add(".*"))
.isInstanceOf(UnsupportedOperationException.class);
}
@Test
public void authorizeShouldReturnFalseForKnownDangerousMethods() 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(
geodeRegExMatchingMethodAuthorizer.authorize(method, testBean)).isFalse());
}
@Test
public void authorizeShouldReturnFalseForGeodeMethodsThatAreNotConsideredSafeEvenIfTheyMatchTheUserRegEx()
throws Exception {
Set<Pair<Method, Object>> methods = new HashSet<>();
methods.add(new Pair<>(TestBean.class.getMethod("accessor"), new TestBean()));
methods.add(new Pair<>(TestBean.class.getMethod("mutator"), new TestBean()));
methods.add(new Pair<>(BucketDump.class.getMethod("getValues"), mock(BucketDump.class)));
methods.add(new Pair<>(RegionConfig.class.getMethod("getEntries"), mock(RegionConfig.class)));
methods.add(new Pair<>(EntryEvent.class.getMethod("getKey"), mock(EntryEvent.class)));
methods.add(new Pair<>(Valuable.class.getMethod("getValue"), mock(Valuable.class)));
methods.add(new Pair<>(HAContainerMap.class.getMethod("containsKey", Object.class),
mock(HAContainerMap.class)));
methods.add(new Pair<>(HAContainerMap.class.getMethod("entrySet"), mock(HAContainerMap.class)));
methods.add(new Pair<>(HAContainerMap.class.getMethod("get", Object.class),
mock(HAContainerMap.class)));
methods.add(new Pair<>(HAContainerMap.class.getMethod("keySet"), mock(HAContainerMap.class)));
methods.add(new Pair<>(HAContainerMap.class.getMethod("values"), mock(HAContainerMap.class)));
methods.forEach(
pair -> assertThat(geodeRegExMatchingMethodAuthorizer.authorize(pair.getX(), pair.getY()))
.as("For method: " + pair.getX().getName()).isFalse());
}
@Test
public void authorizeShouldReturnFalseWheneverTheSecurityServiceDoesNotAllowOperationsOnTheRegion()
throws Exception {
Region region = mock(Region.class);
Region localRegion = mock(LocalRegion.class);
Region partitionedRegion = mock(PartitionedRegion.class);
when(region.getName()).thenReturn("testRegion");
when(localRegion.getName()).thenReturn("testRegion");
when(partitionedRegion.getName()).thenReturn("testRegion");
doThrow(new NotAuthorizedException("Mock Exception")).when(mockSecurityService).authorize(
ResourcePermission.Resource.DATA, ResourcePermission.Operation.READ, "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(geodeRegExMatchingMethodAuthorizer.authorize(method, region)).isFalse();
assertThat(geodeRegExMatchingMethodAuthorizer.authorize(method, localRegion)).isFalse();
assertThat(geodeRegExMatchingMethodAuthorizer.authorize(method, partitionedRegion)).isFalse();
});
}
@Test
public void authorizeShouldReturnTrueForMapMethodsOnRegionInstancesWheneverTheSecurityServiceAllowsOperationsOnTheRegion()
throws Exception {
Region region = mock(Region.class);
Region localRegion = mock(LocalRegion.class);
Region 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(geodeRegExMatchingMethodAuthorizer.authorize(method, region)).isTrue();
assertThat(geodeRegExMatchingMethodAuthorizer.authorize(method, localRegion)).isTrue();
assertThat(geodeRegExMatchingMethodAuthorizer.authorize(method, partitionedRegion)).isTrue();
});
}
@Test
public void authorizeShouldReturnTrueForNonMatchingMethodsConsideredSafeByDefault()
throws Exception {
Set<Pair<Method, Object>> someSafeMethods = new HashSet<>();
someSafeMethods.add(new Pair<>(Object.class.getMethod("toString"), new Object()));
someSafeMethods.add(new Pair<>(Object.class.getMethod("equals", Object.class), new Object()));
someSafeMethods.add(
new Pair<>(Comparable.class.getMethod("compareTo", Object.class), mock(Comparable.class)));
someSafeMethods.add(new Pair<>(Number.class.getMethod("byteValue"), new Integer("0")));
someSafeMethods.add(new Pair<>(Date.class.getMethod("after", Date.class), new Date()));
someSafeMethods.add(new Pair<>(String.class.getMethod("indexOf", int.class), ""));
someSafeMethods.add(new Pair<>(Map.Entry.class.getMethod("getKey"), mock(Map.Entry.class)));
someSafeMethods
.add(new Pair<>(Map.class.getMethod("get", Object.class), Collections.emptyMap()));
RegExMethodAuthorizer authorizer = new RegExMethodAuthorizer(
new RestrictedMethodAuthorizer(mockCache), Collections.emptySet());
someSafeMethods.forEach(pair -> assertThat(authorizer.authorize(pair.getX(), pair.getY()))
.as("For method: " + pair.getX().getName()).isTrue());
}
@Test
public void authorizeShouldReturnFalseForNonMatchingMethodsThatAreNotConsideredSafeByDefault()
throws Exception {
Set<Pair<Method, Object>> someNonSafeMethods = new HashSet<>();
someNonSafeMethods.add(new Pair<>(Object.class.getMethod("wait"), new Object()));
someNonSafeMethods.add(new Pair<>(Number.class.getMethod("notify"), new Integer("0")));
someNonSafeMethods.add(new Pair<>(Date.class.getMethod("setYear", int.class), new Date()));
someNonSafeMethods.add(new Pair<>(Set.class.getMethod("add", Object.class), new HashSet<>()));
someNonSafeMethods.add(
new Pair<>(Map.Entry.class.getMethod("setValue", Object.class), mock(Map.Entry.class)));
someNonSafeMethods.add(
new Pair<>(Map.class.getMethod("put", Object.class, Object.class), Collections.emptyMap()));
RegExMethodAuthorizer authorizer = new RegExMethodAuthorizer(
new RestrictedMethodAuthorizer(mockCache), Collections.emptySet());
someNonSafeMethods.forEach(pair -> assertThat(authorizer.authorize(pair.getX(), pair.getY()))
.as("For method: " + pair.getX().getName()).isFalse());
}
@Test
public void authorizeShouldReturnTrueForMatchingMethods() throws Exception {
RestrictedMethodAuthorizer defaultAuthorizer = new RestrictedMethodAuthorizer(mockCache);
Set<String> regularExpressions =
new HashSet<>(Arrays.asList("^java\\.lang\\..*\\.(?:wait|notify)",
"^java\\.util\\..*\\.(?:set|put|add).*", ".*MockitoMock.*"));
RegExMethodAuthorizer customAuthorizer =
new RegExMethodAuthorizer(defaultAuthorizer, regularExpressions);
Set<Pair<Method, Object>> someNonSafeMethods = new HashSet<>();
someNonSafeMethods.add(new Pair<>(Object.class.getMethod("wait"), new Object()));
someNonSafeMethods.add(new Pair<>(Number.class.getMethod("notify"), new Integer("0")));
someNonSafeMethods.add(new Pair<>(Date.class.getMethod("setYear", int.class), new Date()));
someNonSafeMethods.add(new Pair<>(Set.class.getMethod("add", Object.class), new HashSet<>()));
someNonSafeMethods.add(
new Pair<>(Map.Entry.class.getMethod("setValue", Object.class), mock(Map.Entry.class)));
someNonSafeMethods
.add(new Pair<>(Map.class.getMethod("put", Object.class, Object.class), new HashMap<>()));
someNonSafeMethods
.forEach(pair -> assertThat(customAuthorizer.authorize(pair.getX(), pair.getY()))
.as("For method: " + pair.getX().getName()).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();
}
}
public void mutator() {}
public void accessor() {}
}
}