You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@geode.apache.org by jj...@apache.org on 2019/10/07 14:01:41 UTC
[geode] branch develop updated: GEODE-6988: Implement
JavaBeanAccessorMethodAuthorizer (#4116)
This is an automated email from the ASF dual-hosted git repository.
jjramos pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/geode.git
The following commit(s) were added to refs/heads/develop by this push:
new bbe3259 GEODE-6988: Implement JavaBeanAccessorMethodAuthorizer (#4116)
bbe3259 is described below
commit bbe3259f6ebecfaf9fd7fc8cc0129c33f3c3fbbf
Author: Donal Evans <do...@pivotal.io>
AuthorDate: Mon Oct 7 07:01:22 2019 -0700
GEODE-6988: Implement JavaBeanAccessorMethodAuthorizer (#4116)
- Made the class final, immutable and thread safe.
- Added comprehensive javadocs to the class and its methods.
- Added several unit tests for the class and all public methods.
---
.../integrationTest/resources/assembly_content.txt | 1 +
.../security/JavaBeanAccessorMethodAuthorizer.java | 173 ++++++++++++++++
.../JavaBeanAccessorMethodAuthorizerTest.java | 219 +++++++++++++++++++++
3 files changed, 393 insertions(+)
diff --git a/geode-assembly/src/integrationTest/resources/assembly_content.txt b/geode-assembly/src/integrationTest/resources/assembly_content.txt
index 46f387c..2b39fe2 100644
--- a/geode-assembly/src/integrationTest/resources/assembly_content.txt
+++ b/geode-assembly/src/integrationTest/resources/assembly_content.txt
@@ -493,6 +493,7 @@ javadoc/org/apache/geode/cache/query/package-summary.html
javadoc/org/apache/geode/cache/query/package-tree.html
javadoc/org/apache/geode/cache/query/security/MethodInvocationAuthorizer.html
javadoc/org/apache/geode/cache/query/security/RestrictedMethodAuthorizer.html
+javadoc/org/apache/geode/cache/query/security/JavaBeanAccessorMethodAuthorizer.html
javadoc/org/apache/geode/cache/query/security/UnrestrictedMethodAuthorizer.html
javadoc/org/apache/geode/cache/query/security/package-frame.html
javadoc/org/apache/geode/cache/query/security/package-summary.html
diff --git a/geode-core/src/main/java/org/apache/geode/cache/query/security/JavaBeanAccessorMethodAuthorizer.java b/geode-core/src/main/java/org/apache/geode/cache/query/security/JavaBeanAccessorMethodAuthorizer.java
new file mode 100644
index 0000000..90a2695
--- /dev/null
+++ b/geode-core/src/main/java/org/apache/geode/cache/query/security/JavaBeanAccessorMethodAuthorizer.java
@@ -0,0 +1,173 @@
+/*
+ * 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 java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.Objects;
+import java.util.Properties;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+import org.apache.geode.cache.Cache;
+import org.apache.geode.cache.Declarable;
+import org.apache.geode.cache.Region;
+
+/**
+ * An immutable and thread-safe {@link MethodInvocationAuthorizer} that allows any method execution
+ * that follows the design patterns for accessor methods described in the JavaBean specification
+ * 1.01; that is, any method whose name begins with 'get' or 'is'. For additional security, only
+ * methods belonging to classes in user-specified packages will be allowed. If a method does not
+ * match the user-specified parameters, or belongs to the 'org.apache.geode' package, then the
+ * decision of whether to authorize or not will be delegated to the default
+ * {@link RestrictedMethodAuthorizer}.
+ * <p/>
+ *
+ * Some known dangerous methods, like {@link Object#getClass()}, are also rejected by this
+ * authorizer implementation (see
+ * {@link RestrictedMethodAuthorizer#isKnownDangerousMethod(Method, Object)}).
+ * <p/>
+ *
+ * When used as intended, with all region entries and OQL bind parameters following the JavaBean
+ * specification 1.01, this authorizer implementation addresses all four of the known security
+ * risks: {@code Java Reflection}, {@code Cache Modification}, {@code Region Modification} and
+ * {@code Region Entry Modification}.
+ * <p/>
+ *
+ * It should be noted that the {@code Region Entry Modification} security risk still potentially
+ * exists: users with the {@code DATA:READ:RegionName} privilege will be able to execute any
+ * method whose name starts with 'is' or 'get' on the objects stored within the region and on
+ * instances used as bind parameters of the OQL, providing they are in the specified packages.
+ * If those methods do not fully follow the JavaBean 1.01 specification that accessors do not
+ * modify the instance's state then entry modifications are possible.
+ * <p/>
+ *
+ * Usage of this authorizer implementation is only recommended for secured clusters on which the
+ * Operator has full confidence that all objects stored in regions and used as OQL bind parameters
+ * follow JavaBean specification 1.01. It might also be used on clusters on which the entries
+ * stored are immutable.
+ * <p/>
+ *
+ * @see org.apache.geode.cache.Cache
+ * @see org.apache.geode.cache.query.security.MethodInvocationAuthorizer
+ * @see org.apache.geode.cache.query.security.RestrictedMethodAuthorizer
+ */
+
+public final class JavaBeanAccessorMethodAuthorizer implements MethodInvocationAuthorizer {
+ static final String NULL_PACKAGE_MESSAGE =
+ "A set of allowed packages should be provided to configure the authorizer.";
+ static final String NULL_AUTHORIZER_MESSAGE =
+ "RestrictedMethodAuthorizer should be provided to create this authorizer.";
+ static final String NULL_CACHE_MESSAGE = "Cache should be provided to configure the authorizer.";
+ static final String GEODE_BASE_PACKAGE = "org.apache.geode";
+
+ private static final Pattern pattern = Pattern.compile("^(get|is)($|[^A-Z])+");
+
+ private final RestrictedMethodAuthorizer restrictedMethodAuthorizer;
+ private final Set<String> allowedPackages;
+
+ /**
+ * Creates a {@code JavaBeanAccessorMethodAuthorizer} object and initializes it so it can be
+ * safely used in a multi-threaded environment.
+ * <p/>
+ *
+ * Applications can use this constructor as part of the initialization for custom authorizers
+ * (see {@link Declarable#initialize(Cache, Properties)}), when using a declarative approach.
+ *
+ * @param cache the {@code Cache} instance that owns this authorizer, required in order to
+ * configure the default {@link RestrictedMethodAuthorizer}.
+ * @param allowedPackages the packages containing classes for which 'is' and 'get' methods will
+ * be authorized.
+ */
+ public JavaBeanAccessorMethodAuthorizer(Cache cache, Set<String> allowedPackages) {
+ Objects.requireNonNull(cache, NULL_CACHE_MESSAGE);
+ Objects.requireNonNull(allowedPackages, NULL_PACKAGE_MESSAGE);
+ this.restrictedMethodAuthorizer = new RestrictedMethodAuthorizer(cache);
+ this.allowedPackages = Collections.unmodifiableSet(allowedPackages);
+ }
+
+ /**
+ * Creates a {@code JavaBeanAccessorMethodAuthorizer} object and initializes it so it can be
+ * safely used in a multi-threaded environment.
+ * <p/>
+ *
+ * @param restrictedMethodAuthorizer the default {@code RestrictedMethodAuthorizer} to use.
+ * @param allowedPackages the packages containing classes for which 'is' and 'get' methods will
+ * be authorized.
+ */
+ public JavaBeanAccessorMethodAuthorizer(RestrictedMethodAuthorizer restrictedMethodAuthorizer,
+ Set<String> allowedPackages) {
+ Objects.requireNonNull(restrictedMethodAuthorizer, NULL_AUTHORIZER_MESSAGE);
+ Objects.requireNonNull(allowedPackages, NULL_PACKAGE_MESSAGE);
+ this.restrictedMethodAuthorizer = restrictedMethodAuthorizer;
+ this.allowedPackages = Collections.unmodifiableSet(allowedPackages);
+ }
+
+ /**
+ * Executes the authorization logic to determine whether the {@code method} is allowed to be
+ * executed on the {@code target} object instance.
+ * If the {@code target} object is an instance of {@link Region}, this methods also ensures that
+ * the user has the {@code DATA:READ} permission granted for the target {@link Region}.
+ * </p>
+ *
+ * @param method the {@link Method} that should be authorized.
+ * @param target the {@link Object} on which the {@link Method} will be executed.
+ * @return {@code true} if the {@code method} can be executed on on the {@code target} instance,
+ * {@code false} otherwise.
+ *
+ * @see org.apache.geode.cache.query.security.MethodInvocationAuthorizer
+ */
+ @Override
+ public boolean authorize(Method method, Object target) {
+
+ // Return false for known dangerous methods.
+ if (restrictedMethodAuthorizer.isKnownDangerousMethod(method, target)) {
+ return false;
+ }
+
+ String packageName = target.getClass().getPackage().getName();
+
+ // If the target object belongs to the 'org.apache.geode' package, delegate to the default
+ // authorizer.
+ if (packageName.startsWith(GEODE_BASE_PACKAGE)) {
+ return restrictedMethodAuthorizer.isAllowedGeodeMethod(method, target);
+ }
+
+ String methodName = method.getName();
+
+ // Return true if the the object belongs to an allowed package and the method starts with
+ // exactly 'get' or 'is' followed by a non-lowercase character (to prevent matching with
+ // methods that might have names like 'getawayDate' or 'islandName' etc.)
+ if (pattern.matcher(methodName).matches()) {
+ if (allowedPackages.stream().anyMatch(packageName::startsWith)) {
+ return true;
+ }
+ }
+
+ // Delegate to the default authorizer if none of the above criteria are met.
+ return restrictedMethodAuthorizer.authorize(method, target);
+ }
+
+ /**
+ * Returns an unmodifiable view of the allowed packages for this authorizer.
+ * This method can be used to get "read-only" access to the set containing the packages
+ * specified as allowed on construction of this authorizer.
+ *
+ * @return an unmodifiable view of the allowed packages for this authorizer.
+ */
+ Set<String> getAllowedPackages() {
+ return allowedPackages;
+ }
+}
diff --git a/geode-core/src/test/java/org/apache/geode/cache/query/security/JavaBeanAccessorMethodAuthorizerTest.java b/geode-core/src/test/java/org/apache/geode/cache/query/security/JavaBeanAccessorMethodAuthorizerTest.java
new file mode 100644
index 0000000..dfe6f7c
--- /dev/null
+++ b/geode-core/src/test/java/org/apache/geode/cache/query/security/JavaBeanAccessorMethodAuthorizerTest.java
@@ -0,0 +1,219 @@
+/*
+ * 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.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+
+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.HashSet;
+import java.util.List;
+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.internal.cache.InternalCache;
+import org.apache.geode.test.junit.categories.SecurityTest;
+
+@Category(SecurityTest.class)
+public class JavaBeanAccessorMethodAuthorizerTest {
+ private InternalCache mockCache;
+ private JavaBeanAccessorMethodAuthorizer authorizerWithStringPackageSpecified;
+ private RestrictedMethodAuthorizer defaultAuthorizer;
+ private final String STRING_PACKAGE = String.class.getPackage().getName();
+
+ @Before
+ public void setUp() {
+ mockCache = mock(InternalCache.class);
+ defaultAuthorizer = new RestrictedMethodAuthorizer(mockCache);
+
+ Set<String> allowedPackages = new HashSet<>();
+ allowedPackages.add(STRING_PACKAGE);
+
+ authorizerWithStringPackageSpecified =
+ new JavaBeanAccessorMethodAuthorizer(defaultAuthorizer, allowedPackages);
+ }
+
+ @Test
+ public void constructorThrowsExceptionWhenCacheIsNull() {
+ assertThatThrownBy(() -> new JavaBeanAccessorMethodAuthorizer((Cache) null, new HashSet<>()))
+ .isInstanceOf(NullPointerException.class)
+ .hasMessage(JavaBeanAccessorMethodAuthorizer.NULL_CACHE_MESSAGE);
+ }
+
+ @Test
+ public void constructorThrowsExceptionWhenRestrictedMethodAuthorizerIsNull() {
+ assertThatThrownBy(() -> new JavaBeanAccessorMethodAuthorizer((RestrictedMethodAuthorizer) null,
+ new HashSet<>()))
+ .isInstanceOf(NullPointerException.class)
+ .hasMessage(JavaBeanAccessorMethodAuthorizer.NULL_AUTHORIZER_MESSAGE);
+ }
+
+ @Test
+ public void constructorsThrowsExceptionWhenAllowedPackagesIsNull() {
+ assertThatThrownBy(() -> new JavaBeanAccessorMethodAuthorizer(mockCache, null))
+ .isInstanceOf(NullPointerException.class)
+ .hasMessage(JavaBeanAccessorMethodAuthorizer.NULL_PACKAGE_MESSAGE);
+
+ assertThatThrownBy(
+ () -> new JavaBeanAccessorMethodAuthorizer(defaultAuthorizer, null))
+ .isInstanceOf(NullPointerException.class)
+ .hasMessage(JavaBeanAccessorMethodAuthorizer.NULL_PACKAGE_MESSAGE);
+ }
+
+ @Test
+ public void authorizeReturnsFalseForKnownDangerousMethods() throws NoSuchMethodException {
+ 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(authorizerWithStringPackageSpecified.authorize(method, testBean))
+ .isFalse());
+ }
+
+ @Test
+ public void authorizeReturnsFalseForDisallowedGeodeClassesWithGeodePackageSpecified()
+ throws NoSuchMethodException {
+ TestBean testBean = new TestBean();
+
+ assertThat((testBean.getClass().getPackage().getName()))
+ .startsWith(JavaBeanAccessorMethodAuthorizer.GEODE_BASE_PACKAGE);
+
+ List<Method> geodeMethods = new ArrayList<>();
+ geodeMethods.add(TestBean.class.getMethod("isMatchingMethod"));
+ geodeMethods.add(TestBean.class.getMethod("getMatchingMethod"));
+ geodeMethods.add(TestBean.class.getMethod("nonMatchingMethod"));
+
+ Set<String> geodePackage = new HashSet<>();
+ geodePackage.add(JavaBeanAccessorMethodAuthorizer.GEODE_BASE_PACKAGE);
+ JavaBeanAccessorMethodAuthorizer geodeMatchingAuthorizer =
+ new JavaBeanAccessorMethodAuthorizer(defaultAuthorizer, geodePackage);
+
+ geodeMethods.forEach(
+ method -> assertThat(geodeMatchingAuthorizer.authorize(method, testBean)).isFalse());
+ }
+
+ @Test
+ public void authorizeReturnsFalseForMatchingMethodNamesAndNonMatchingPackage()
+ throws NoSuchMethodException {
+ List testList = new ArrayList();
+
+ Method getMatchingMethod = List.class.getMethod("get", int.class);
+ Method isMatchingMethod = List.class.getMethod("isEmpty");
+
+ assertThat(authorizerWithStringPackageSpecified.authorize(isMatchingMethod, testList))
+ .isFalse();
+ assertThat(authorizerWithStringPackageSpecified.authorize(getMatchingMethod, testList))
+ .isFalse();
+ }
+
+ @Test
+ public void authorizeReturnsFalseForNonMatchingMethodNameAndMatchingPackage()
+ throws NoSuchMethodException {
+ String testString = "";
+
+ Method method = String.class.getMethod("notify");
+
+ assertThat(authorizerWithStringPackageSpecified.authorize(method, testString)).isFalse();
+ }
+
+ @Test
+ public void authorizeReturnsTrueForMatchingMethodNamesAndPackage() throws NoSuchMethodException {
+ String testString = "";
+
+ Method isMatchingMethod = String.class.getMethod("isEmpty");
+ Method getMatchingMethod = String.class.getMethod("getBytes");
+
+ assertThat(authorizerWithStringPackageSpecified.authorize(isMatchingMethod, testString))
+ .isTrue();
+ assertThat(authorizerWithStringPackageSpecified.authorize(getMatchingMethod, testString))
+ .isTrue();
+ }
+
+ @Test
+ public void authorizeReturnsFalseForNonMatchingDisallowedMethod() throws NoSuchMethodException {
+ Object object = new Object();
+
+ Method method = Object.class.getMethod("notify");
+
+ assertThat(authorizerWithStringPackageSpecified.authorize(method, object)).isFalse();
+ }
+
+ @Test
+ public void authorizeReturnsTrueForNonMatchingAllowedMethod() throws NoSuchMethodException {
+ Object object = new Object();
+
+ Method method = Object.class.getMethod("equals", Object.class);
+
+ assertThat(authorizerWithStringPackageSpecified.authorize(method, object)).isTrue();
+ }
+
+ @Test
+ public void allowedPackagesIsUnmodifiable() {
+ assertThatThrownBy(
+ () -> authorizerWithStringPackageSpecified.getAllowedPackages().remove(STRING_PACKAGE))
+ .isInstanceOf(UnsupportedOperationException.class);
+ }
+
+
+ @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 isMatchingMethod() {}
+
+ public void getMatchingMethod() {}
+
+ public void nonMatchingMethod() {}
+ }
+
+}