From 9f9aa6975a45a7eadd67bd4a3bbf0eadd4a07f43 Mon Sep 17 00:00:00 2001 From: John Blum Date: Fri, 7 Sep 2018 19:27:43 -0700 Subject: [PATCH] SGF-787 - Add 'requiredPermissions' to @GemfireFunction annotation. --- .../function/GemfireFunctionUtils.java | 177 ++++++-- .../gemfire/function/PojoFunctionWrapper.java | 43 +- .../function/annotation/GemfireFunction.java | 58 ++- .../GemfireFunctionBeanPostProcessor.java | 24 +- ...ireFunctionBeanPostProcessorRegistrar.java | 5 +- ...rmissionsRegistrationIntegrationTests.java | 82 ++++ .../GemfireFunctionUtilsUnitTests.java | 400 ++++++++++++++++++ 7 files changed, 733 insertions(+), 56 deletions(-) create mode 100644 src/test/java/org/springframework/data/gemfire/function/FunctionWithRequiredPermissionsRegistrationIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/gemfire/function/GemfireFunctionUtilsUnitTests.java diff --git a/src/main/java/org/springframework/data/gemfire/function/GemfireFunctionUtils.java b/src/main/java/org/springframework/data/gemfire/function/GemfireFunctionUtils.java index 9d08489e..b8a4610b 100644 --- a/src/main/java/org/springframework/data/gemfire/function/GemfireFunctionUtils.java +++ b/src/main/java/org/springframework/data/gemfire/function/GemfireFunctionUtils.java @@ -24,11 +24,13 @@ import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.geode.cache.execute.Function; import org.apache.geode.cache.execute.FunctionService; +import org.apache.geode.security.ResourcePermission; import org.springframework.beans.BeanUtils; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.AnnotationUtils; @@ -50,6 +52,8 @@ import org.springframework.util.StringUtils; */ public abstract class GemfireFunctionUtils { + private static final String DEFAULT_FUNCTION_ID = null; + private static Log log = LogFactory.getLog(GemfireFunctionUtils.class); /** @@ -61,19 +65,39 @@ public abstract class GemfireFunctionUtils { * @see java.lang.reflect.Method */ public static boolean isGemfireFunction(Method method) { - return (method != null && method.isAnnotationPresent(GemfireFunction.class)); + return method != null && method.isAnnotationPresent(GemfireFunction.class); } - private static boolean isMatchingGemfireFunction(Method method, String functionId) { + /** + * Determines whether the given {@link Method} is a POJO, {@link GemfireFunction} annotated {@link Method} + * having an ID matching the given {@code functionId}. + * + * @param method {@link Method} to evaluate. + * @param functionId {@link String} containing the {@link Function#getId() ID} to match. + * @return a boolean indicating whether the given {@link Method} is a POJO, {@link GemfireFunction} annotated + * {@link Method} with the given {@code functionId}. + * @see #getGemfireFunctionId(Method) + * @see java.lang.reflect.Method + */ + public static boolean isMatchingGemfireFunction(Method method, String functionId) { return getGemfireFunctionId(method).filter(methodFunctionId -> ObjectUtils.nullSafeEquals(methodFunctionId, functionId)).isPresent(); } - private static AnnotationAttributes getAnnotationAttributes(AnnotatedElement element, + static Annotation getAnnotation(AnnotatedElement element, Class annotationType) { + return AnnotationUtils.getAnnotation(element, annotationType); + } + + static AnnotationAttributes getAnnotationAttributes(AnnotatedElement element, Annotation annotation) { + return AnnotationAttributes.fromMap(AnnotationUtils.getAnnotationAttributes(element, annotation)); + } + + static AnnotationAttributes getAnnotationAttributes(AnnotatedElement element, Class annotationType) { - return AnnotationAttributes.fromMap( - AnnotationUtils.getAnnotationAttributes(element, element.getAnnotation(annotationType))); + return element.isAnnotationPresent(annotationType) + ? getAnnotationAttributes(element, getAnnotation(element, annotationType)) + : null; } /** @@ -83,24 +107,24 @@ public abstract class GemfireFunctionUtils { * If the {@link Method} is not {@literal null} and annotated with {@link GemfireFunction} then this method * tries to determine the {@link Function#getId()} from the {@link GemfireFunction#id()} attribute. * - * If the {@link GemfireFunction#id()} attribute was not set, then the {@link Method#getName()} + * If the {@link GemfireFunction#id()} attribute was not explicitly set, then the {@link Method#getName()} * is returned as the {@link Function#getId() Function ID}. * * @param method {@link GemfireFunction} annotated POJO {@link Method} containing the implementation - * for a Pivotal GemFire/Apache Geode {@link Function}. - * @return the Pivotal GemFire/Apache Geode Function ID of the given {@link Method}, or an empty {@link Optional} - * if the {@link Method} is not a Pivotal GemFire/Apache Geode {@link Function}. + * of the GemFire/Geode {@link Function}. + * @return the GemFire/Geode {@link Function#getId() Function ID} of the given {@link Method}, + * or an empty {@link Optional} if the {@link Method} is not a GemFire/Geode {@link Function}. * @see org.springframework.data.gemfire.function.annotation.GemfireFunction * @see java.lang.reflect.Method * @see #isGemfireFunction(Method) */ - private static Optional getGemfireFunctionId(Method method) { + @SuppressWarnings("all") + public static Optional getGemfireFunctionId(Method method) { return Optional.ofNullable(method) .filter(GemfireFunctionUtils::isGemfireFunction) .map(it -> - Optional.of(it.getAnnotation(GemfireFunction.class)) - .map(annotation -> getAnnotationAttributes(it, annotation.getClass())) + Optional.ofNullable(getAnnotationAttributes(it, GemfireFunction.class)) .filter(annotationAttributes -> annotationAttributes.containsKey("id")) .map(annotationAttributes -> annotationAttributes.getString("id")) .filter(StringUtils::hasText) @@ -114,7 +138,8 @@ public abstract class GemfireFunctionUtils { .map(constructor -> BeanUtils.instantiateClass(constructor, constructorArguments)) .orElseThrow(() -> newIllegalArgumentException( "No suitable constructor was found for type [%s] having parameters [%s]", type.getName(), - stream(nullSafeArray(constructorArguments, Object.class)).map(ObjectUtils::nullSafeClassName) + stream(nullSafeArray(constructorArguments, Object.class)) + .map(ObjectUtils::nullSafeClassName) .collect(Collectors.toList()))); } @@ -160,6 +185,7 @@ public abstract class GemfireFunctionUtils { * @see #registerFunctionForPojoMethod(Object, Method, AnnotationAttributes, boolean) * @see #isMatchingGemfireFunction(Method, String) */ + @SuppressWarnings("unused") public static void registerFunctionForPojoMethod(Object target, String functionId) { Assert.notNull(target, "Target object is required"); @@ -183,6 +209,7 @@ public abstract class GemfireFunctionUtils { * @see #registerFunctionForPojoMethod(Object, Method, AnnotationAttributes, boolean) * @see #isGemfireFunction(Method) */ + @SuppressWarnings("unused") public static void registerFunctionForPojoMethod(Object target, Method method, boolean overwrite) { Assert.notNull(target, "Target object is required"); @@ -206,48 +233,153 @@ public abstract class GemfireFunctionUtils { public static void registerFunctionForPojoMethod(Object target, Method method, AnnotationAttributes gemfireFunctionAttributes, boolean overwrite) { - String id = gemfireFunctionAttributes.containsKey("id") - ? gemfireFunctionAttributes.getString("id") : ""; + PojoFunctionWrapper function = + new PojoFunctionWrapper(target, method, resolveFunctionId(gemfireFunctionAttributes)); - PojoFunctionWrapper function = new PojoFunctionWrapper(target, method, id); + configureBatchSize(target, method, gemfireFunctionAttributes, function); + configureHighAvailability(gemfireFunctionAttributes, function); + configureHasResult(gemfireFunctionAttributes, function); + configureOptimizeForWrite(gemfireFunctionAttributes, function); + configureRequiredPermissions(gemfireFunctionAttributes, function); + + doFunctionRegistration(function, overwrite); + } + + static String resolveFunctionId(AnnotationAttributes gemfireFunctionAttributes) { + + return gemfireFunctionAttributes.containsKey("id") + ? gemfireFunctionAttributes.getString("id") + : DEFAULT_FUNCTION_ID; + } + + static void configureBatchSize(Object target, Method method, AnnotationAttributes gemfireFunctionAttributes, + PojoFunctionWrapper function) { if (gemfireFunctionAttributes.containsKey("batchSize")) { int batchSize = gemfireFunctionAttributes.getNumber("batchSize"); Assert.isTrue(batchSize >= 0, - String.format("batchSize [%1$d] specified on [%2$s.%3$s] must be a non-negative value", - batchSize, target.getClass().getName(), method.getName())); + String.format("%1$s.batchSize [%2$d] specified on [%3$s.%4$s] must be a non-negative value", + GemfireFunction.class.getSimpleName(), batchSize, target.getClass().getName(), method.getName())); function.setBatchSize(batchSize); } + } + + static void configureHighAvailability(AnnotationAttributes gemfireFunctionAttributes, + PojoFunctionWrapper function) { if (gemfireFunctionAttributes.containsKey("HA")) { function.setHA(gemfireFunctionAttributes.getBoolean("HA")); } + } + + static void configureHasResult(AnnotationAttributes gemfireFunctionAttributes, PojoFunctionWrapper function) { if (gemfireFunctionAttributes.containsKey("hasResult")) { if (Boolean.TRUE.equals(gemfireFunctionAttributes.getBoolean("hasResult"))) { function.setHasResult(true); } } + } + + static void configureOptimizeForWrite(AnnotationAttributes gemfireFunctionAttributes, + PojoFunctionWrapper function) { if (gemfireFunctionAttributes.containsKey("optimizeForWrite")) { function.setOptimizeForWrite(gemfireFunctionAttributes.getBoolean("optimizeForWrite")); } + } + + static void configureRequiredPermissions(AnnotationAttributes gemfireFunctionAttributes, + PojoFunctionWrapper function) { + + String[] requiredPermissionStrings = nullSafeArray(gemfireFunctionAttributes.containsKey("requiredPermissions") + ? gemfireFunctionAttributes.getStringArray("requiredPermissions") + : null, String.class); + + Stream requiredPermissionsStream = Arrays.stream(requiredPermissionStrings); + + Optional.of(requiredPermissionsStream + .filter(StringUtils::hasText) + .map(GemfireFunctionUtils::parseResourcePermission) + .collect(Collectors.toList())) + .filter(requiredPermissions -> !requiredPermissions.isEmpty()) + .ifPresent(function::setRequiredPermissions); + } + + static ResourcePermission parseResourcePermission(String resourcePermissionString) { + + Assert.hasText(resourcePermissionString, + String.format("ResourcePermission [%s] is required", resourcePermissionString)); + + ResourcePermission.Resource resource = ResourcePermission.Resource.DATA; + ResourcePermission.Operation operation = ResourcePermission.Operation.WRITE; + + String key = null; + String target = null; + + String[] resourcePermissionStringComponents = resourcePermissionString.split(":"); + + if (resourcePermissionStringComponents.length > 0) { + resource = parseEnum(resourcePermissionStringComponents[0], ResourcePermission.Resource.class, + ResourcePermission.Resource.values()); + } + if (resourcePermissionStringComponents.length > 1) { + operation = parseEnum(resourcePermissionStringComponents[1], ResourcePermission.Operation.class, + ResourcePermission.Operation.values()); + } + if (resourcePermissionStringComponents.length > 2) { + target = nullSafeTrim(resourcePermissionStringComponents[2]); + } + if (resourcePermissionStringComponents.length > 3) { + key = nullSafeTrim(resourcePermissionStringComponents[3]); + } + + return new ResourcePermission(resource, operation, target, key); + } + + private static String nullSafeToUpperCase(String value) { + return StringUtils.hasText(value) ? value.trim().toUpperCase() : "null"; + } + + private static String nullSafeTrim(String value) { + return value != null ? value.trim() : null; + } + + private static > T parseEnum(String name, Class enumType, Enum[] enumeratedValues) { + + name = nullSafeToUpperCase(name); + + try { + return Enum.valueOf(enumType, name); + } + catch (IllegalArgumentException cause) { + throw newIllegalArgumentException(cause, "[%1$s] is not a valid [%2$s] type; must be 1 of %3$s", + name, enumType.getName(), Arrays.toString(enumeratedValues)); + } + } + + static void doFunctionRegistration(PojoFunctionWrapper function, boolean overwrite) { if (FunctionService.isRegistered(function.getId())) { if (overwrite) { if (log.isDebugEnabled()) { - log.debug(String.format("Unregistering Function [%s]", function.getId())); + log.debug(String.format("Overwrite enabled; Unregistering Function [%s]", function.getId())); } FunctionService.unregisterFunction(function.getId()); } } - if (!FunctionService.isRegistered(function.getId())) { + if (FunctionService.isRegistered(function.getId())) { + if (log.isDebugEnabled()) { + log.debug(String.format("Function [%s] is already registered", function.getId())); + } + } + else { FunctionService.registerFunction(function); @@ -255,11 +387,6 @@ public abstract class GemfireFunctionUtils { log.debug(String.format("Registered Function [%s]", function.getId())); } } - else { - if (log.isDebugEnabled()) { - log.debug(String.format("Function [%s] is already registered", function.getId())); - } - } } /** diff --git a/src/main/java/org/springframework/data/gemfire/function/PojoFunctionWrapper.java b/src/main/java/org/springframework/data/gemfire/function/PojoFunctionWrapper.java index ef1f38e4..cb5e60e7 100644 --- a/src/main/java/org/springframework/data/gemfire/function/PojoFunctionWrapper.java +++ b/src/main/java/org/springframework/data/gemfire/function/PojoFunctionWrapper.java @@ -12,7 +12,11 @@ */ package org.springframework.data.gemfire.function; +import static org.springframework.data.gemfire.util.CollectionUtils.asSet; + import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Collections; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -20,6 +24,8 @@ import org.apache.geode.cache.Region; import org.apache.geode.cache.execute.Function; import org.apache.geode.cache.execute.FunctionContext; import org.apache.geode.cache.execute.ResultSender; +import org.apache.geode.management.internal.security.ResourcePermissions; +import org.apache.geode.security.ResourcePermission; import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; @@ -52,6 +58,8 @@ public class PojoFunctionWrapper implements Function { private volatile int batchSize; + private Collection requiredPermissions = asSet(ResourcePermissions.DATA_WRITE); + private final FunctionArgumentResolver functionArgumentResolver; private final Method method; @@ -60,21 +68,41 @@ public class PojoFunctionWrapper implements Function { private final String id; + public PojoFunctionWrapper(Object target, Method method) { + this(target, method, null); + } + public PojoFunctionWrapper(Object target, Method method, String id) { this.target = target; this.method = method; - this.id = StringUtils.hasText(id) ? id : method.getName(); - this.functionArgumentResolver = new FunctionContextInjectingArgumentResolver(method); + this.id = resolveId(method, id); + this.functionArgumentResolver = newFunctionArgumentResolver(method); this.HA = false; - this.hasResult = !method.getReturnType().equals(void.class); + this.hasResult = resolveHasResult(method); this.optimizeForWrite = false; } + protected FunctionArgumentResolver newFunctionArgumentResolver(Method method) { + return new FunctionContextInjectingArgumentResolver(method); + } + + protected boolean resolveHasResult(Method method) { + return !method.getReturnType().equals(void.class); + } + + protected String resolveId(Method method, String id) { + return StringUtils.hasText(id) ? id : method.getName(); + } + public void setBatchSize(int batchSize) { this.batchSize = batchSize; } + public int getBatchSize() { + return this.batchSize; + } + public void setHA(boolean HA) { this.HA = HA; } @@ -107,6 +135,15 @@ public class PojoFunctionWrapper implements Function { return this.optimizeForWrite; } + public void setRequiredPermissions(Collection requiredPermissions) { + this.requiredPermissions = requiredPermissions; + } + + @Override + public Collection getRequiredPermissions(String regionName) { + return Collections.unmodifiableCollection(this.requiredPermissions); + } + @Override @SuppressWarnings("unchecked") public void execute(FunctionContext functionContext) { diff --git a/src/main/java/org/springframework/data/gemfire/function/annotation/GemfireFunction.java b/src/main/java/org/springframework/data/gemfire/function/annotation/GemfireFunction.java index cbb66853..0842cb3f 100644 --- a/src/main/java/org/springframework/data/gemfire/function/annotation/GemfireFunction.java +++ b/src/main/java/org/springframework/data/gemfire/function/annotation/GemfireFunction.java @@ -17,37 +17,33 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.apache.geode.cache.execute.Function; +import org.apache.geode.cache.execute.ResultSender; + /** * - * Used to declare a concrete method as a Pivotal GemFire function implementation + * Used to declare a concrete method as a Pivotal GemFire {@link Function} implementation. * * @author David Turanski - * + * @author John Blum + * @see org.apache.geode.cache.execute.Function */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) public @interface GemfireFunction { + String DEFAULT_RESOURCE_PERMISSION = "DATA:WRITE"; + /** - * The name of the registered function. If not provided the simple method name will be used - * @return the function id + * The {@link String name} of the registered {@link Function}. + * + * If not provided the simple {@link String method name} will be used. + * + * @return the {@link Function} {@link Function#getId() id}. + * @see org.apache.geode.cache.execute.Function#getId() */ String id() default ""; - /** - * Attribute to determine whether the Pivotal GemFire Function is HA (Highly Available). - * - * @return a boolean value indicating whether the defined Pivotal GemFire Function is HA. - */ - boolean HA() default false; - - /** - * Attribute to determine whether the Pivotal GemFire Function is optimized for write operations. - * - * @return a boolean value indicating if the Pivotal GemFire Function is configured for optimized write operations. - */ - boolean optimizeForWrite() default false; - /** * Controls the maximum number of results sent at one time. * @@ -56,11 +52,31 @@ public @interface GemfireFunction { int batchSize() default 0; /** - * Normally follows the method return type, i.e., false if void, true otherwise. This allows overriding - * a void method which uses the resultSender directly. + * Attribute used to configure whether the {@link Function} is HA (Highly Available). * - * @return a boolean value indicating if the Pivotal GemFire Function is expected to return a result. + * @return a boolean value configuring whether the defined {@link Function} is HA. + * @see org.apache.geode.cache.execute.Function#isHA() + */ + boolean HA() default false; + + /** + * Normally follows the method return type, i.e. {@literal false} if {@code void}, {@literal true} otherwise. + * + * This allows overriding a {@code void} method which uses the {@link ResultSender} directly. + * + * @return a boolean value indicating if the {@link Function} is expected to return a result. + * @see org.apache.geode.cache.execute.Function#hasResult() */ boolean hasResult() default false; + /** + * Attribute to configure whether the {@link Function} is optimized for write operations. + * + * @return a boolean value indicating if the {@link Function} is configured for optimized write operations. + * @see org.apache.geode.cache.execute.Function#optimizeForWrite() + */ + boolean optimizeForWrite() default false; + + String[] requiredPermissions() default { DEFAULT_RESOURCE_PERMISSION }; + } diff --git a/src/main/java/org/springframework/data/gemfire/function/config/GemfireFunctionBeanPostProcessor.java b/src/main/java/org/springframework/data/gemfire/function/config/GemfireFunctionBeanPostProcessor.java index eacd1d73..7a8d1a2a 100644 --- a/src/main/java/org/springframework/data/gemfire/function/config/GemfireFunctionBeanPostProcessor.java +++ b/src/main/java/org/springframework/data/gemfire/function/config/GemfireFunctionBeanPostProcessor.java @@ -15,9 +15,11 @@ package org.springframework.data.gemfire.function.config; import static java.util.Arrays.stream; import static org.springframework.data.gemfire.util.ArrayUtils.nullSafeArray; +import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import org.apache.geode.cache.execute.Function; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.core.annotation.AnnotationAttributes; @@ -28,17 +30,21 @@ import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; /** - * Spring {@link BeanPostProcessor} that discovers bean components wired as Function implementations, - * i.e. beans containing methods annotated with {@link GemfireFunction}. + * Spring {@link BeanPostProcessor} that discovers bean components configured as {@link Function} implementations, + * i.e. beans containing {@link Method methods} annotated with {@link GemfireFunction}. * * @author David Turanski * @author John Blum + * @see java.lang.annotation.Annotation + * @see java.lang.reflect.Method + * @see org.apache.geode.cache.execute.Function * @see org.springframework.beans.factory.config.BeanPostProcessor * @see org.springframework.data.gemfire.function.annotation.GemfireFunction */ public class GemfireFunctionBeanPostProcessor implements BeanPostProcessor { - /* (non-Javadoc) + /* + * (non-Javadoc) * @see org.springframework.beans.factory.config.BeanPostProcessor#postProcessAfterInitialization(java.lang.Object, java.lang.String) */ @Override @@ -61,11 +67,17 @@ public class GemfireFunctionBeanPostProcessor implements BeanPostProcessor { String.format("The bean [%s] method [%s] annotated with [%s] must be public", bean.getClass().getName(), method.getName(), GemfireFunction.class.getName())); - AnnotationAttributes annotationAttributes = AnnotationAttributes.fromMap( - AnnotationUtils.getAnnotationAttributes(gemfireFunctionAnnotation,false,true)); + AnnotationAttributes gemfireFunctionAttributes = resolveAnnotationAttributes(gemfireFunctionAnnotation); - GemfireFunctionUtils.registerFunctionForPojoMethod(bean, method, annotationAttributes, false); + GemfireFunctionUtils.registerFunctionForPojoMethod(bean, method, + gemfireFunctionAttributes, false); } }); } + + private AnnotationAttributes resolveAnnotationAttributes(Annotation annotation) { + + return AnnotationAttributes.fromMap(AnnotationUtils.getAnnotationAttributes(annotation, + false, true)); + } } diff --git a/src/main/java/org/springframework/data/gemfire/function/config/GemfireFunctionBeanPostProcessorRegistrar.java b/src/main/java/org/springframework/data/gemfire/function/config/GemfireFunctionBeanPostProcessorRegistrar.java index 47ff68e3..f942f899 100644 --- a/src/main/java/org/springframework/data/gemfire/function/config/GemfireFunctionBeanPostProcessorRegistrar.java +++ b/src/main/java/org/springframework/data/gemfire/function/config/GemfireFunctionBeanPostProcessorRegistrar.java @@ -30,7 +30,10 @@ public class GemfireFunctionBeanPostProcessorRegistrar implements ImportBeanDefi */ @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { - BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(GemfireFunctionBeanPostProcessor.class); + + BeanDefinitionBuilder builder = + BeanDefinitionBuilder.genericBeanDefinition(GemfireFunctionBeanPostProcessor.class); + BeanDefinitionReaderUtils.registerWithGeneratedName(builder.getBeanDefinition(), registry); } } diff --git a/src/test/java/org/springframework/data/gemfire/function/FunctionWithRequiredPermissionsRegistrationIntegrationTests.java b/src/test/java/org/springframework/data/gemfire/function/FunctionWithRequiredPermissionsRegistrationIntegrationTests.java new file mode 100644 index 00000000..18e95e5c --- /dev/null +++ b/src/test/java/org/springframework/data/gemfire/function/FunctionWithRequiredPermissionsRegistrationIntegrationTests.java @@ -0,0 +1,82 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed 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.springframework.data.gemfire.function; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.lang.annotation.Annotation; + +import org.apache.geode.cache.execute.Function; +import org.apache.geode.cache.execute.FunctionService; +import org.apache.geode.security.ResourcePermission; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.gemfire.function.annotation.GemfireFunction; +import org.springframework.data.gemfire.function.config.EnableGemfireFunctions; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * Integration tests testing the registration of Apache Geode/Pivotal GemFire {@link Function Functions} + * with required permissions specified using the {@link GemfireFunction} {@link Annotation}. + * + * @author John Blum + * @see org.junit.Test + * @see org.springframework.context.annotation.Bean + * @see org.springframework.context.annotation.Configuration + * @see org.springframework.data.gemfire.function.annotation.GemfireFunction + * @see org.springframework.data.gemfire.function.config.EnableGemfireFunctions + * @see org.springframework.test.context.ContextConfiguration + * @see org.springframework.test.context.junit4.SpringRunner + * @since 2.1.0 + */ +@RunWith(SpringRunner.class) +@ContextConfiguration +@SuppressWarnings({ "unchecked", "unused" }) +public class FunctionWithRequiredPermissionsRegistrationIntegrationTests { + + @Test + public void functionsRegisteredWithRequiredPermissionsSuccessfully() { + + Function function = FunctionService.getFunction("testFunctionWithRequiredPermissions"); + + assertThat(function).isNotNull(); + assertThat(function.getRequiredPermissions("test")).containsExactly( + new ResourcePermission(ResourcePermission.Resource.CLUSTER, ResourcePermission.Operation.MANAGE), + new ResourcePermission(ResourcePermission.Resource.DATA, ResourcePermission.Operation.READ, "Example") + ); + } + + @Configuration + @EnableGemfireFunctions + static class TestConfiguration { + + @Bean + TestFunctions testFunctions() { + return new TestFunctions(); + } + } + + static class TestFunctions { + + @GemfireFunction(requiredPermissions = { "CLUSTER:MANAGE", "DATA:READ:Example" }) + public void testFunctionWithRequiredPermissions() { } + + } +} diff --git a/src/test/java/org/springframework/data/gemfire/function/GemfireFunctionUtilsUnitTests.java b/src/test/java/org/springframework/data/gemfire/function/GemfireFunctionUtilsUnitTests.java new file mode 100644 index 00000000..a377cde6 --- /dev/null +++ b/src/test/java/org/springframework/data/gemfire/function/GemfireFunctionUtilsUnitTests.java @@ -0,0 +1,400 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed 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.springframework.data.gemfire.function; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.lang.reflect.Method; +import java.util.Arrays; + +import org.apache.geode.security.ResourcePermission; +import org.junit.Test; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.data.gemfire.function.annotation.GemfireFunction; + +/** + * Unit tests for {@link GemfireFunctionUtils}. + * + * @author John Blum + * @see org.junit.Test + * @see org.springframework.data.gemfire.function.GemfireFunctionUtils + * @since 2.1.0 + */ +@SuppressWarnings("unused") +public class GemfireFunctionUtilsUnitTests { + + private final TestFunctions testFunctions = new TestFunctions(); + + private ResourcePermission newResourcePermission(ResourcePermission.Resource resource, + ResourcePermission.Operation operation, ResourcePermission.Target target, String key) { + + return new ResourcePermission(resource, operation, target.name(), key); + } + + private ResourcePermission newResourcePermission(ResourcePermission.Resource resource, + ResourcePermission.Operation operation, String target, String key) { + + return new ResourcePermission(resource, operation, target, key); + } + + @Test + public void gemfireFunctionAnnotatedMethodIsGemFireFunction() throws Exception { + assertThat(GemfireFunctionUtils.isGemfireFunction(TestFunctions.class.getDeclaredMethod("testFunction"))) + .isTrue(); + } + + @Test + public void nonGemfireFunctionAnnotatedMethodIsNotGemFireFunction() throws Exception { + assertThat(GemfireFunctionUtils.isGemfireFunction(TestFunctions.class.getDeclaredMethod("nonFunction"))) + .isFalse(); + } + + @Test + public void matchingGemFireFunctionReturnsTrue() throws Exception { + assertThat(GemfireFunctionUtils.isMatchingGemfireFunction( + TestFunctions.class.getDeclaredMethod("identifiedFunction"), "MyFunction")).isTrue(); + } + + @Test + public void nonMatchingGemFireFunctionReturnsFalse() throws Exception { + + assertThat(GemfireFunctionUtils.isMatchingGemfireFunction( + TestFunctions.class.getDeclaredMethod("identifiedFunction"), "YourFunction")) + .isFalse(); + + assertThat(GemfireFunctionUtils.isMatchingGemfireFunction( + TestFunctions.class.getDeclaredMethod("nonFunction"), "nonFunction")) + .isFalse(); + + assertThat(GemfireFunctionUtils.isMatchingGemfireFunction( + TestFunctions.class.getDeclaredMethod("requiredPermissionsFunction"), "MyFunction")) + .isFalse(); + + assertThat(GemfireFunctionUtils.isMatchingGemfireFunction( + TestFunctions.class.getDeclaredMethod("testFunction"), "MockFunction")) + .isFalse(); + } + + @Test + public void resolvesFunctionIdFromGemfireFunctionIdAttribute() { + + AnnotationAttributes gemfireFunctionAttributes = new AnnotationAttributes(); + + gemfireFunctionAttributes.put("id", "2"); + + assertThat(GemfireFunctionUtils.resolveFunctionId(gemfireFunctionAttributes)).isEqualTo("2"); + } + + @Test + public void unresolvableFunctionIdReturnsNull() { + assertThat(GemfireFunctionUtils.resolveFunctionId(new AnnotationAttributes())).isNull(); + } + + @Test + public void configureWithBatchSize() throws Exception { + + Method functionWithBatchSize = TestFunctions.class.getDeclaredMethod("functionWithBatchSize"); + + AnnotationAttributes gemfireFunctionAttributes = + GemfireFunctionUtils.getAnnotationAttributes(functionWithBatchSize, GemfireFunction.class); + + PojoFunctionWrapper function = new PojoFunctionWrapper(this.testFunctions, functionWithBatchSize); + + GemfireFunctionUtils.configureBatchSize(this.testFunctions, functionWithBatchSize, + gemfireFunctionAttributes, function); + + assertThat(function.getBatchSize()).isEqualTo(10); + } + + @Test(expected = IllegalArgumentException.class) + public void configureWithInvalidBatchSizeThrowsIllegalArgumentException() throws Exception { + + Method functionWithInvalidBatchSize = + TestFunctions.class.getDeclaredMethod("functionWithInvalidBatchSize"); + + AnnotationAttributes gemfireFunctionAttributes = + GemfireFunctionUtils.getAnnotationAttributes(functionWithInvalidBatchSize, GemfireFunction.class); + + PojoFunctionWrapper function = new PojoFunctionWrapper(this.testFunctions, functionWithInvalidBatchSize); + + try { + GemfireFunctionUtils.configureBatchSize(this.testFunctions, functionWithInvalidBatchSize, + gemfireFunctionAttributes, function); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("GemfireFunction.batchSize [-5] specified on [%1$s.%2$s] must be a non-negative value", + testFunctions.getClass().getName(), functionWithInvalidBatchSize.getName()); + + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test + public void configureWithNoBatchSize() throws Exception { + + Method testFunction = TestFunctions.class.getDeclaredMethod("testFunction"); + + AnnotationAttributes gemfireFunctionAttributes = + GemfireFunctionUtils.getAnnotationAttributes(testFunction, GemfireFunction.class); + + PojoFunctionWrapper function = new PojoFunctionWrapper(this.testFunctions, testFunction); + + GemfireFunctionUtils.configureBatchSize(this.testFunctions, testFunction, gemfireFunctionAttributes, function); + + assertThat(function.getBatchSize()).isEqualTo(0); + } + + @Test + public void configureWithHighAvailability() throws Exception { + + Method functionWithHighAvailability = TestFunctions.class.getDeclaredMethod("functionWithHighAvailability"); + + AnnotationAttributes gemfireFunctionAttributes = + GemfireFunctionUtils.getAnnotationAttributes(functionWithHighAvailability, GemfireFunction.class); + + PojoFunctionWrapper function = new PojoFunctionWrapper(this.testFunctions, functionWithHighAvailability); + + GemfireFunctionUtils.configureHighAvailability(gemfireFunctionAttributes, function); + + assertThat(function.isHA()).isTrue(); + } + + @Test + public void configureWithNoHighAvailability() throws Exception { + + Method testFunction = TestFunctions.class.getDeclaredMethod("testFunction"); + + AnnotationAttributes gemfireFunctionAttributes = + GemfireFunctionUtils.getAnnotationAttributes(testFunction, GemfireFunction.class); + + PojoFunctionWrapper function = new PojoFunctionWrapper(this.testFunctions, testFunction); + + GemfireFunctionUtils.configureHighAvailability(gemfireFunctionAttributes, function); + + assertThat(function.isHA()).isFalse(); + } + + @Test + public void configureWithResult() throws Exception { + + Method functionWithResult = TestFunctions.class.getDeclaredMethod("functionWithResult"); + + AnnotationAttributes gemfireFunctionAttributes = + GemfireFunctionUtils.getAnnotationAttributes(functionWithResult, GemfireFunction.class); + + PojoFunctionWrapper function = new PojoFunctionWrapper(this.testFunctions, functionWithResult); + + GemfireFunctionUtils.configureHasResult(gemfireFunctionAttributes, function); + + assertThat(function.hasResult()).isTrue(); + } + + @Test + public void configureWithNoResult() throws Exception { + + Method testFunction = TestFunctions.class.getDeclaredMethod("testFunction"); + + AnnotationAttributes gemfireFunctionAttributes = + GemfireFunctionUtils.getAnnotationAttributes(testFunction, GemfireFunction.class); + + PojoFunctionWrapper function = new PojoFunctionWrapper(this.testFunctions, testFunction); + + GemfireFunctionUtils.configureHasResult(gemfireFunctionAttributes, function); + + assertThat(function.hasResult()).isFalse(); + } + + @Test + public void configureWithOptimizeForWrite() throws Exception { + + Method functionOptimizedForWrite = TestFunctions.class.getDeclaredMethod("functionOptimizedForWrite"); + + AnnotationAttributes gemfireFunctionAttributes = + GemfireFunctionUtils.getAnnotationAttributes(functionOptimizedForWrite, GemfireFunction.class); + + PojoFunctionWrapper function = new PojoFunctionWrapper(this.testFunctions, functionOptimizedForWrite); + + GemfireFunctionUtils.configureOptimizeForWrite(gemfireFunctionAttributes, function); + + assertThat(function.optimizeForWrite()).isTrue(); + } + + @Test + public void configureWithNoOptimizeForWrite() throws Exception { + + Method testFunction = TestFunctions.class.getDeclaredMethod("testFunction"); + + AnnotationAttributes gemfireFunctionAttributes = + GemfireFunctionUtils.getAnnotationAttributes(testFunction, GemfireFunction.class); + + PojoFunctionWrapper function = new PojoFunctionWrapper(this.testFunctions, testFunction); + + GemfireFunctionUtils.configureOptimizeForWrite(gemfireFunctionAttributes, function); + + assertThat(function.optimizeForWrite()).isFalse(); + } + + @Test + public void cofigureWithDefaultRequiredPermissions() throws Exception { + + Method testFunction = TestFunctions.class.getDeclaredMethod("testFunction"); + + AnnotationAttributes gemfireFunctionAttributes = + GemfireFunctionUtils.getAnnotationAttributes(testFunction, GemfireFunction.class); + + PojoFunctionWrapper function = new PojoFunctionWrapper(this.testFunctions, testFunction); + + GemfireFunctionUtils.configureRequiredPermissions(gemfireFunctionAttributes, function); + + assertThat(function.getRequiredPermissions("test")).containsExactly( + newResourcePermission(ResourcePermission.Resource.DATA, ResourcePermission.Operation.WRITE, (String) null, null) + ); + } + + @Test + public void configureWithRequiredPermissions() throws Exception { + + Method requiredPermissionsFunction = TestFunctions.class.getDeclaredMethod("requiredPermissionsFunction"); + + AnnotationAttributes gemfireFunctionAttributes = + GemfireFunctionUtils.getAnnotationAttributes(requiredPermissionsFunction, GemfireFunction.class); + + PojoFunctionWrapper function = new PojoFunctionWrapper(this.testFunctions, requiredPermissionsFunction); + + GemfireFunctionUtils.configureRequiredPermissions(gemfireFunctionAttributes, function); + + assertThat(function.getRequiredPermissions("Example")).containsExactly( + newResourcePermission(ResourcePermission.Resource.CLUSTER, ResourcePermission.Operation.MANAGE, (String) null, null), + newResourcePermission(ResourcePermission.Resource.DATA, ResourcePermission.Operation.MANAGE, "System", null), + newResourcePermission(ResourcePermission.Resource.DATA, ResourcePermission.Operation.READ, ResourcePermission.Target.QUERY, "ALL"), + newResourcePermission(ResourcePermission.Resource.DATA, ResourcePermission.Operation.WRITE, "Example", "123") + ); + } + + public void testParseResourcePermissionWithInvalidArgumentThrowsIllegalArgumentException( + String resourcePermissionString) { + + try { + GemfireFunctionUtils.parseResourcePermission(resourcePermissionString); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("ResourcePermission [%s] is required", resourcePermissionString); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test(expected = IllegalArgumentException.class) + public void parseResourcePermissionWithNullStringThrowsIllegalArgumentException() { + testParseResourcePermissionWithInvalidArgumentThrowsIllegalArgumentException(null); + } + + @Test(expected = IllegalArgumentException.class) + public void parseResourcePermissionWithEmptyStringThrowsIllegalArgumentException() { + testParseResourcePermissionWithInvalidArgumentThrowsIllegalArgumentException(""); + } + + @Test(expected = IllegalArgumentException.class) + public void parseResourcePermissionWithBlankStringThrowsIllegalArgumentException() { + testParseResourcePermissionWithInvalidArgumentThrowsIllegalArgumentException(" "); + } + + @Test(expected = IllegalArgumentException.class) + public void parseResourcePermissionWithInvalidResourceThrowsIllegalArgumentException() { + + try { + GemfireFunctionUtils.parseResourcePermission("SOCKET:OPEN"); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("[SOCKET] is not a valid [%1$s] type; must be 1 of %2$s", + ResourcePermission.Resource.class.getName(), Arrays.toString(ResourcePermission.Resource.values())); + + assertThat(expected).hasCauseInstanceOf(IllegalArgumentException.class); + + throw expected; + } + } + + @Test(expected = IllegalArgumentException.class) + public void parseResourcePermissionWithInvalidOperationThrowsIllegalArgumentException() { + + try { + GemfireFunctionUtils.parseResourcePermission("DATA:COPY"); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("[COPY] is not a valid [%1$s] type; must be 1 of %2$s", + ResourcePermission.Operation.class.getName(), Arrays.toString(ResourcePermission.Operation.values())); + + assertThat(expected).hasCauseInstanceOf(IllegalArgumentException.class); + + throw expected; + } + } + + @Test + public void parseResourcePermissionIsSuccessful() { + + ResourcePermission resourcePermission = + GemfireFunctionUtils.parseResourcePermission("CLUSTER:MANAGE"); + + assertThat(resourcePermission).isNotNull(); + assertThat(resourcePermission.getResource()).isEqualTo(ResourcePermission.Resource.CLUSTER); + assertThat(resourcePermission.getOperation()).isEqualTo(ResourcePermission.Operation.MANAGE); + } + + static class TestFunctions { + + @GemfireFunction(batchSize = 10) + void functionWithBatchSize() { } + + @GemfireFunction(batchSize = -5) + void functionWithInvalidBatchSize() { } + + @GemfireFunction(HA = true) + void functionWithHighAvailability() { } + + @GemfireFunction(hasResult = true) + void functionWithResult() { } + + @GemfireFunction(optimizeForWrite = true) + void functionOptimizedForWrite() { } + + @GemfireFunction(id = "MyFunction") + void identifiedFunction() { } + + void nonFunction() { } + + @GemfireFunction(requiredPermissions = { + "CLUSTER:MANAGE", + "DATA:MANAGE:System", + "DATA:READ:QUERY:ALL", + "DATA:WRITE:Example:123" + }) + void requiredPermissionsFunction() { } + + @GemfireFunction + void testFunction() { } + + } +}