From e4e32e7dcf2feb8e7d77ac4a720ad30f214a2d69 Mon Sep 17 00:00:00 2001 From: Andjelko Perisic <66070554+anjeyy@users.noreply.github.com> Date: Sat, 23 Jan 2021 20:51:53 +0100 Subject: [PATCH] grpc server-side validation --- .../grpc/common/util/InterceptorOrder.java | 4 + .../server/validation/GrpcConstraint.java | 42 +++++++ .../validation/GrpcConstraintIsPresent.java | 49 ++++++++ .../validation/GrpcConstraintValidator.java | 44 +++++++ .../validation/GrpcValidationConfig.java | 52 ++++++++ .../validation/GrpcValidationResolver.java | 111 ++++++++++++++++++ .../RequestValidationInterceptor.java | 52 ++++++++ .../validation/RequestValidationListener.java | 101 ++++++++++++++++ 8 files changed, 455 insertions(+) create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/validation/GrpcConstraint.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/validation/GrpcConstraintIsPresent.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/validation/GrpcConstraintValidator.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/validation/GrpcValidationConfig.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/validation/GrpcValidationResolver.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/validation/RequestValidationInterceptor.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/validation/RequestValidationListener.java diff --git a/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/util/InterceptorOrder.java b/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/util/InterceptorOrder.java index 205b2a492..948464f7d 100644 --- a/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/util/InterceptorOrder.java +++ b/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/util/InterceptorOrder.java @@ -54,6 +54,10 @@ public final class InterceptorOrder { * The order value for security interceptors related to authorization checks. */ public static final int ORDER_SECURITY_AUTHORISATION = 5200; + /** + * The order value for validating incoming server requests interceptors. + */ + public static final int ORDER_SERVER_REQUEST_VALIDATION = 15000; /** * The order value for interceptors that should be executed last. This is equivalent to * {@link Ordered#LOWEST_PRECEDENCE}. This is the default for interceptors without specified priority. diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/validation/GrpcConstraint.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/validation/GrpcConstraint.java new file mode 100644 index 000000000..07e33e351 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/validation/GrpcConstraint.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2016-2021 Michael Zhang + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package net.devh.boot.grpc.server.validation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.stereotype.Component; + +/** + * Marker annotation to scan for validation classes, which have to implement {@link GrpcConstraintValidator}. Scanning + * is done in {@link GrpcValidationResolver}. + * + * @author Andjelko Perisic (andjelko.perisic@gmail.com) + * @see GrpcConstraintValidator + * @see GrpcValidationResolver + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Component +public @interface GrpcConstraint { + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/validation/GrpcConstraintIsPresent.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/validation/GrpcConstraintIsPresent.java new file mode 100644 index 000000000..bddc78658 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/validation/GrpcConstraintIsPresent.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2016-2021 Michael Zhang + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package net.devh.boot.grpc.server.validation; + +import java.util.Objects; + +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.context.annotation.ConfigurationCondition; +import org.springframework.core.type.AnnotatedTypeMetadata; + +/** + * Condition checking if annotation {@link GrpcConstraint @GrpcConstraint} is present. Used to indicate that classes can + * be picked up for validation purpose with {@link GrpcValidationResolver}. + * + * @author Andjelko Perisic (andjelko.perisic@gmail.com) + * @see GrpcConstraint + * @see GrpcValidationResolver + */ +class GrpcConstraintIsPresent implements ConfigurationCondition { + + @Override + public ConfigurationPhase getConfigurationPhase() { + return ConfigurationPhase.REGISTER_BEAN; + } + + @Override + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + + ConfigurableListableBeanFactory safeBeanFactory = + Objects.requireNonNull(context.getBeanFactory(), "ConfigurableListableBeanFactory is null"); + return !safeBeanFactory.getBeansWithAnnotation(GrpcConstraint.class).isEmpty(); + } +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/validation/GrpcConstraintValidator.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/validation/GrpcConstraintValidator.java new file mode 100644 index 000000000..acccaca70 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/validation/GrpcConstraintValidator.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2016-2021 Michael Zhang + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package net.devh.boot.grpc.server.validation; + +import com.google.protobuf.MessageLiteOrBuilder; + +import net.devh.boot.grpc.server.service.GrpcService; + +/** + * Implement this interface to perform a request validation for incoming gRPC messages. Subsequently requests received + * in {@link GrpcService @GrpcService} are validated.
+ * Hint: Also annotate class with {@link GrpcConstraint @GrpcConstraint} to be picked up. + * + * @author Andjelko Perisic (andjelko.perisic@gmail.com) + * @see GrpcValidationResolver + * @see GrpcConstraint + */ +public interface GrpcConstraintValidator { + + /** + * Method invoked to check wheter validation succeds. In case an exeception occurs a + * {@link io.grpc.Status.Code#INTERNAL} is sent back to the client with the thrown exception message. + * + * @param request gRPC request + * @return {@code true} if validation successfull, {@code false otherwise} + */ + boolean isValid(E request); + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/validation/GrpcValidationConfig.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/validation/GrpcValidationConfig.java new file mode 100644 index 000000000..b3ec60275 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/validation/GrpcValidationConfig.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2016-2021 Michael Zhang + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package net.devh.boot.grpc.server.validation; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; + +import net.devh.boot.grpc.common.util.InterceptorOrder; +import net.devh.boot.grpc.server.interceptor.GrpcGlobalServerInterceptor; + +/** + * In Order to have valid requests this autoconfiguration is looking for marker annotation + * {@link GrpcConstraint @GrpcConstraint}. In case of success, all necessary beans are being instantiated. + * + * @author Andjelko Perisic (andjelko.perisic@gmail.com) + * @see GrpcConstraint + * @see GrpcValidationResolver + * @see RequestValidationInterceptor + */ +@Configuration +@Conditional(GrpcConstraintIsPresent.class) +class GrpcValidationConfig { + + @Bean + GrpcValidationResolver grpcValidationResolver() { + return new GrpcValidationResolver(); + } + + @GrpcGlobalServerInterceptor + @Order(InterceptorOrder.ORDER_SERVER_REQUEST_VALIDATION) + RequestValidationInterceptor requestValidationInterceptor(final GrpcValidationResolver grpcValidationResolver) { + return new RequestValidationInterceptor(grpcValidationResolver); + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/validation/GrpcValidationResolver.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/validation/GrpcValidationResolver.java new file mode 100644 index 000000000..947d13cf0 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/validation/GrpcValidationResolver.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2016-2021 Michael Zhang + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package net.devh.boot.grpc.server.validation; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; + +import com.google.protobuf.MessageLiteOrBuilder; + +import lombok.extern.slf4j.Slf4j; + +/** + * Resolving all classes implementing {@link GrpcConstraintValidator} and marked with annotation + * {@link GrpcConstraint @GrpcConstraint}. Resolved classes are validation classes for gRPC requests to be validated. + *

+ * The Validation is done via {@link RequestValidationInterceptor}. There can be more than one validation class for the + * same request type, all of them are being resolved and used for validation. + * + * @author Andjelko Perisic (andjelko.perisic@gmail.com) + * @see GrpcConstraintValidator + * @see RequestValidationInterceptor + */ +@Slf4j +class GrpcValidationResolver implements InitializingBean, ApplicationContextAware { + + private Map> validatorMap; + private ApplicationContext applicationContext; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + @Override + public void afterPropertiesSet() throws Exception { + + validatorMap = applicationContext.getBeansWithAnnotation(GrpcConstraint.class) + .entrySet() + .stream() + .collect(Collectors.toMap(Entry::getKey, this::convertSafely)); + log.debug("Found {} gRPC validators", validatorMap.size()); + } + + + private GrpcConstraintValidator convertSafely(Map.Entry entry) { + + Object annotatedValidator = entry.getValue(); + if (annotatedValidator instanceof GrpcConstraintValidator) { + @SuppressWarnings("unchecked") + GrpcConstraintValidator safeConstraintInstance = + (GrpcConstraintValidator) annotatedValidator; + return safeConstraintInstance; + } + + throw new IllegalStateException( + String.format("@GrpcConstraint annotated class [%s] has to implement GrpcConstraintValidator.class", + annotatedValidator.getClass())); + } + + /** + * Retrieve all {@link GrpcConstraintValidator} which are the same class or at least a superclass of given input + * parameter. + * + * @param request gRPC request + * @param type of the gRPC request message + * @return validators to be used in conjunction with the request + */ + List> findValidators(E request) { + return validatorMap.values() + .stream() + .filter(cs -> checkForGenericTypeArgument(cs, request)) + .collect(Collectors.toList()); + } + + private boolean checkForGenericTypeArgument( + GrpcConstraintValidator grpcConstraintValidator, E request) { + + List genericTypes = Arrays.asList(grpcConstraintValidator.getClass().getGenericInterfaces()); + + return genericTypes.stream() + .map(t -> (ParameterizedType) t) + .flatMap(pt -> Arrays.stream(pt.getActualTypeArguments())) + .map(t -> (Class) t) + .anyMatch(c -> c.isAssignableFrom(request.getClass())); + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/validation/RequestValidationInterceptor.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/validation/RequestValidationInterceptor.java new file mode 100644 index 000000000..4c6bc9e66 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/validation/RequestValidationInterceptor.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2016-2021 Michael Zhang + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package net.devh.boot.grpc.server.validation; + +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCall.Listener; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; + +/** + * Interceptor to validate incoming gRPC requests. Validations are obtained from {@link GrpcValidationResolver} and + * processed with {@link RequestValidationListener}. + * + * @author Andjelko Perisic (andjelko.perisic@gmail.com) + * @see RequestValidationListener + * @see GrpcValidationResolver + */ +class RequestValidationInterceptor implements ServerInterceptor { + + private final GrpcValidationResolver grpcValidationResolver; + + RequestValidationInterceptor(final GrpcValidationResolver grpcValidationResolver) { + this.grpcValidationResolver = grpcValidationResolver; + } + + @Override + public Listener interceptCall( + ServerCall call, + Metadata headers, + ServerCallHandler next) { + + Listener delegate = next.startCall(call, headers); + return new RequestValidationListener<>(delegate, call, headers, grpcValidationResolver); + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/validation/RequestValidationListener.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/validation/RequestValidationListener.java new file mode 100644 index 000000000..ab1b3268f --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/validation/RequestValidationListener.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2016-2021 Michael Zhang + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package net.devh.boot.grpc.server.validation; + +import java.util.List; +import java.util.stream.Collectors; + +import com.google.protobuf.MessageLiteOrBuilder; + +import io.grpc.ForwardingServerCallListener.SimpleForwardingServerCallListener; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCall.Listener; +import io.grpc.Status; +import lombok.extern.slf4j.Slf4j; + +/** + * Responsible for a proper server-side validation of incoming gRPC requests. There is no restriction in how many + * validations of a request type can exist. Every one of them is being applied and validated.
+ * When a validation fails, a {@link Status.Code#INVALID_ARGUMENT} is returned with + * {@link ServerCall#close(Status, Metadata)}. In case an exception is raised inside + * {@link GrpcConstraintValidator#isValid(MessageLiteOrBuilder)} the {@code Status} is {@link Status.Code#INTERNAL} with + * a message from raised exception. + * + * @author Andjelko Perisic (andjelko.perisic@gmail.com) + * @see GrpcValidationResolver + */ +@Slf4j +class RequestValidationListener extends SimpleForwardingServerCallListener { + + private final ServerCall serverCall; + private final Metadata headers; + private final GrpcValidationResolver grpcValidationResolver; + + protected RequestValidationListener( + Listener delegate, + ServerCall serverCall, + Metadata headers, + GrpcValidationResolver grpcValidationResolver) { + super(delegate); + this.serverCall = serverCall; + this.headers = headers; + this.grpcValidationResolver = grpcValidationResolver; + } + + @Override + public void onMessage(ReqT message) { + + List> validatorList = + grpcValidationResolver.findValidators(message); + MessageLiteOrBuilder convertedMessage = (MessageLiteOrBuilder) message; + boolean requestIsNotValid = validatorList.stream().anyMatch(v -> isNotValid(v, convertedMessage)); + + if (requestIsNotValid) { + handleInvalidRequest(validatorList); + } else { + super.onMessage(message); + } + } + + private boolean isNotValid( + GrpcConstraintValidator validator, + MessageLiteOrBuilder convertedMessage) { + + try { + return !validator.isValid(convertedMessage); + } catch (Throwable t) { + log.error("Error during validation: " + t); + Status status = Status.INTERNAL.withDescription(t.getMessage()); + serverCall.close(status, headers); + return true; + } + } + + private void handleInvalidRequest(List> validatorList) { + + String validators = validatorList.stream() + .map(v -> v.getClass().getSimpleName()) + .collect(Collectors.joining(", ")); + + String errorMsg = String.format("Validation error at least in one of [%s]", validators); + Status status = Status.INVALID_ARGUMENT.withDescription(errorMsg); + serverCall.close(status, headers); + } + +}