blob: cfe33565c68de2ff56805c677d76e95561bd7f33 [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 java.lang.reflect.Method;
import java.sql.Timestamp;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;
import org.apache.geode.annotations.Immutable;
import org.apache.geode.cache.Cache;
import org.apache.geode.cache.Declarable;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.query.internal.QRegion;
import org.apache.geode.internal.cache.InternalCache;
import org.apache.geode.internal.security.SecurityService;
import org.apache.geode.internal.security.SecurityServiceFactory;
import org.apache.geode.security.NotAuthorizedException;
import org.apache.geode.security.ResourcePermission;
/**
* The default, immutable and thread-safe {@link MethodInvocationAuthorizer} used by Geode to
* determine whether a {@link java.lang.reflect.Method} is allowed to be executed on a specific
* {@link java.lang.Object} instance.
* <p/>
*
* This authorizer addresses the four known security risks: {@code Java Reflection},
* {@code Cache Modification}, {@code Region Modification} and {@code Region Entry Modification}.
* <p/>
*
* Custom applications can delegate to this class and use it as the starting point for providing
* use case specific authorizers.
*
* @see org.apache.geode.cache.Cache
* @see org.apache.geode.cache.query.security.MethodInvocationAuthorizer
*/
public final class RestrictedMethodAuthorizer implements MethodInvocationAuthorizer {
public static final String UNAUTHORIZED_STRING = "Unauthorized access to method: ";
@Immutable
static final Set<String> FORBIDDEN_METHODS =
Collections.unmodifiableSet(createForbiddenList());
@Immutable
static final Map<String, Set<Class>> GEODE_ALLOWED_METHODS =
Collections.unmodifiableMap(createGeodeAcceptanceList());
@Immutable
static final Map<String, Set<Class>> DEFAULT_ALLOWED_METHODS =
Collections.unmodifiableMap(createDefaultAcceptanceList());
final SecurityService securityService;
private final Set<String> forbiddenMethods;
private final Map<String, Set<Class>> allowedMethodsPerClass;
private final Map<String, Set<Class>> allowedGeodeMethodsPerClass;
private static Set<String> createForbiddenList() {
Set<String> forbiddenList = new HashSet<>();
// Reflection Calls
forbiddenList.add("getClass");
// Serialization Calls
forbiddenList.add("readObject");
forbiddenList.add("readResolve");
forbiddenList.add("readObjectNoData");
forbiddenList.add("writeObject");
forbiddenList.add("writeReplace");
return forbiddenList;
}
private static Map<String, Set<Class>> createGeodeAcceptanceList() {
Map<String, Set<Class>> acceptanceListMap = new HashMap<>();
Set<Class> objectCallers = new HashSet<>();
objectCallers.add(Object.class);
objectCallers = Collections.unmodifiableSet(objectCallers);
acceptanceListMap.put("equals", objectCallers);
acceptanceListMap.put("toString", objectCallers);
Set<Class> entryCallers = new HashSet<>();
entryCallers.add(Region.Entry.class);
entryCallers = Collections.unmodifiableSet(entryCallers);
acceptanceListMap.put("getKey", entryCallers);
acceptanceListMap.put("getValue", entryCallers);
Set<Class> regionCallers = new HashSet<>();
regionCallers.add(Region.class);
regionCallers.add(QRegion.class);
regionCallers = Collections.unmodifiableSet(regionCallers);
acceptanceListMap.put("containsKey", regionCallers);
acceptanceListMap.put("entrySet", regionCallers);
acceptanceListMap.put("get", regionCallers);
acceptanceListMap.put("keySet", regionCallers);
acceptanceListMap.put("values", regionCallers);
acceptanceListMap.put("getEntries", regionCallers);
acceptanceListMap.put("getValues", regionCallers);
return acceptanceListMap;
}
private static Map<String, Set<Class>> createDefaultAcceptanceList() {
Map<String, Set<Class>> acceptanceListMap = new HashMap<>();
Set<Class> objectCallers = new HashSet<>();
objectCallers.add(Object.class);
objectCallers = Collections.unmodifiableSet(objectCallers);
acceptanceListMap.put("compareTo", objectCallers);
acceptanceListMap.put("equals", objectCallers);
acceptanceListMap.put("toString", objectCallers);
Set<Class> booleanCallers = new HashSet<>();
booleanCallers.add(Boolean.class);
booleanCallers = Collections.unmodifiableSet(booleanCallers);
acceptanceListMap.put("booleanValue", booleanCallers);
Set<Class> numericCallers = new HashSet<>();
numericCallers.add(Number.class);
numericCallers = Collections.unmodifiableSet(numericCallers);
acceptanceListMap.put("byteValue", numericCallers);
acceptanceListMap.put("doubleValue", numericCallers);
acceptanceListMap.put("floatValue", numericCallers);
acceptanceListMap.put("intValue", numericCallers);
acceptanceListMap.put("longValue", numericCallers);
acceptanceListMap.put("shortValue", numericCallers);
Set<Class> dateCallers = new HashSet<>();
dateCallers.add(Date.class);
dateCallers = Collections.unmodifiableSet(dateCallers);
acceptanceListMap.put("after", dateCallers);
acceptanceListMap.put("before", dateCallers);
acceptanceListMap.put("getTime", dateCallers);
Set<Class> timestampCallers = new HashSet<>();
timestampCallers.add(Timestamp.class);
timestampCallers = Collections.unmodifiableSet(timestampCallers);
acceptanceListMap.put("getNanos", timestampCallers);
Set<Class> stringCallers = new HashSet<>();
stringCallers.add(String.class);
stringCallers = Collections.unmodifiableSet(stringCallers);
acceptanceListMap.put("charAt", stringCallers);
acceptanceListMap.put("codePointAt", stringCallers);
acceptanceListMap.put("codePointBefore", stringCallers);
acceptanceListMap.put("codePointCount", stringCallers);
acceptanceListMap.put("compareToIgnoreCase", stringCallers);
acceptanceListMap.put("concat", stringCallers);
acceptanceListMap.put("contains", stringCallers);
acceptanceListMap.put("contentEquals", stringCallers);
acceptanceListMap.put("endsWith", stringCallers);
acceptanceListMap.put("equalsIgnoreCase", stringCallers);
acceptanceListMap.put("getBytes", stringCallers);
acceptanceListMap.put("hashCode", stringCallers);
acceptanceListMap.put("indexOf", stringCallers);
acceptanceListMap.put("intern", stringCallers);
acceptanceListMap.put("isEmpty", stringCallers);
acceptanceListMap.put("lastIndexOf", stringCallers);
acceptanceListMap.put("length", stringCallers);
acceptanceListMap.put("matches", stringCallers);
acceptanceListMap.put("offsetByCodePoints", stringCallers);
acceptanceListMap.put("regionMatches", stringCallers);
acceptanceListMap.put("replace", stringCallers);
acceptanceListMap.put("replaceAll", stringCallers);
acceptanceListMap.put("replaceFirst", stringCallers);
acceptanceListMap.put("split", stringCallers);
acceptanceListMap.put("startsWith", stringCallers);
acceptanceListMap.put("substring", stringCallers);
acceptanceListMap.put("toCharArray", stringCallers);
acceptanceListMap.put("toLowerCase", stringCallers);
acceptanceListMap.put("toUpperCase", stringCallers);
acceptanceListMap.put("trim", stringCallers);
Set<Class> mapEntryCallers = new HashSet<>();
mapEntryCallers.add(Map.Entry.class);
mapEntryCallers = Collections.unmodifiableSet(mapEntryCallers);
acceptanceListMap.put("getKey", mapEntryCallers);
acceptanceListMap.put("getValue", mapEntryCallers);
Set<Class> regionCallers = new HashSet<>();
regionCallers.add(Map.class);
regionCallers.add(QRegion.class);
regionCallers = Collections.unmodifiableSet(regionCallers);
acceptanceListMap.put("containsKey", regionCallers);
acceptanceListMap.put("entrySet", regionCallers);
acceptanceListMap.put("get", regionCallers);
acceptanceListMap.put("keySet", regionCallers);
acceptanceListMap.put("values", regionCallers);
acceptanceListMap.put("getEntries", regionCallers);
acceptanceListMap.put("getValues", regionCallers);
return acceptanceListMap;
}
/**
* Returns an unmodifiable view of the methods disallowed by default.
* This method can be used to get "read-only" access to the set containing the methods that
* are considered non safe by default.
*
* @return an unmodifiable view of the default disallowed methods.
*/
Set<String> getForbiddenMethods() {
return forbiddenMethods;
}
/**
* Returns an unmodifiable view of the default allowed methods.
* This method can be used to get "read-only" access to the map containing the default set
* of allowed methods per class.
*
* @return an unmodifiable view of the default allowed methods per class map.
*/
Map<String, Set<Class>> getAllowedMethodsPerClass() {
return allowedMethodsPerClass;
}
/**
* Returns an unmodifiable view of the default Geode allowed methods.
* This method can be used to get "read-only" access to the map containing the default set
* of allowed geode methods per class.
*
* @return an unmodifiable view of the default allowed geode methods per class map.
*/
Map<String, Set<Class>> getAllowedGeodeMethodsPerClass() {
return allowedGeodeMethodsPerClass;
}
/**
* Creates a {@code RestrictedMethodAuthorizer} object and initializes it so it can be safely
* used in a multi-threaded environment.
* <p/>
*
* If the {@link Cache} instance passed as parameter was previously created by Geode, the
* authorizer will use the security service already configured in order to determine whether a
* specific user has read privileges upon a particular region.
* If the {@link Cache} instance passed as parameter is a wrapper created by external frameworks,
* the authorizer will create a new instance of the security service using the configuration
* properties used to initialize the cache.
* <p/>
*
* Applications can also 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 security rules used.
*/
public RestrictedMethodAuthorizer(Cache cache) {
Objects.requireNonNull(cache, "Cache should be provided to configure the authorizer.");
// Set the correct SecurityService.
if (cache instanceof InternalCache) {
// Use the already created SecurityService.
this.securityService = ((InternalCache) cache).getSecurityService();
} else {
// Create the SecurityService using the distributed system properties.
Objects.requireNonNull(cache.getDistributedSystem(),
"Distributed system properties should be provided to configure the authorizer.");
this.securityService =
SecurityServiceFactory.create(cache.getDistributedSystem().getSecurityProperties());
}
this.forbiddenMethods = FORBIDDEN_METHODS;
this.allowedMethodsPerClass = DEFAULT_ALLOWED_METHODS;
this.allowedGeodeMethodsPerClass = GEODE_ALLOWED_METHODS;
}
private boolean isAllowedByDefault(Method method, Object target) {
String methodName = method.getName();
Set<Class> allowedClasses = allowedMethodsPerClass.get(methodName);
if (allowedClasses == null) {
return false;
}
for (Class<?> clazz : allowedClasses) {
if (clazz.isAssignableFrom(target.getClass())) {
return true;
}
}
return false;
}
private void authorizeRegionAccess(SecurityService securityService, Object target) {
if (target instanceof Region) {
String regionName = ((Region) target).getName();
securityService.authorize(ResourcePermission.Resource.DATA, ResourcePermission.Operation.READ,
regionName);
}
}
/**
* Executes the verification logic to determine whether the {@code target} object instance belongs
* to Geode and whether the {@code method} on the {@code target} object instance is considered
* to be safe according to Geode security rules.
* 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 verified.
* @param target the {@link Object} on which the {@link Method} will be executed.
* @return {@code true} if and only if the {@code target} object instance belongs to Geode and
* the {@code method} is considered safe to be executed on the {@code target} object
* instance according to the Geode security rules, {@code false} otherwise.
*/
public boolean isAllowedGeodeMethod(Method method, Object target) {
String methodName = method.getName();
Set<Class> allowedGeodeClassesForMethod = allowedGeodeMethodsPerClass.get(methodName);
if (allowedGeodeClassesForMethod == null) {
return false;
}
for (Class<?> clazz : allowedGeodeClassesForMethod) {
if (clazz.isAssignableFrom(target.getClass())) {
try {
authorizeRegionAccess(securityService, target);
return true;
} catch (NotAuthorizedException noAuthorizedException) {
return false;
}
}
}
return false;
}
/**
* Executes the verification logic to determine whether the {@code method} on the {@code target}
* object instance is considered to be non safe according to Geode security rules.
* <p/>
*
* The following methods are currently considered non safe, no matter what the {@code target}
* object is:
* <p>
* <ul>
* <li>{@code getClass}
* <li>{@code readObject}
* <li>{@code readResolve}
* <li>{@code readObjectNoData}
* <li>{@code writeObject}
* <li>{@code writeReplace}
* </ul>
* <p>
*
* @param method the {@link Method} that should be verified.
* @param target the {@link Object} on which the {@link Method} will be executed.
* @return {@code true} if the {@code method} is considered non safe to be executed on the
* {@code target} instance according to the Geode security rules, {@code false} otherwise.
*/
public boolean isPermanentlyForbiddenMethod(Method method,
@SuppressWarnings("unused") Object target) {
return forbiddenMethods.contains(method.getName());
}
/**
* 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) {
if (!isAllowedByDefault(method, target)) {
return false;
}
try {
authorizeRegionAccess(securityService, target);
} catch (NotAuthorizedException noAuthorizedException) {
return false;
}
return true;
}
}