Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gRPC request validation (server-side) #487

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright (c) 2016-2021 Michael Zhang <yidongnan@gmail.com>
*
* 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 {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright (c) 2016-2021 Michael Zhang <yidongnan@gmail.com>
*
* 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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright (c) 2016-2021 Michael Zhang <yidongnan@gmail.com>
*
* 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.<br>
* <b>Hint: </b> 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<E extends MessageLiteOrBuilder> {

/**
* 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);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright (c) 2016-2021 Michael Zhang <yidongnan@gmail.com>
*
* 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);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* Copyright (c) 2016-2021 Michael Zhang <yidongnan@gmail.com>
*
* 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.
* <p>
* 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<String, GrpcConstraintValidator<MessageLiteOrBuilder>> 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<MessageLiteOrBuilder> convertSafely(Map.Entry<String, Object> entry) {

Object annotatedValidator = entry.getValue();
if (annotatedValidator instanceof GrpcConstraintValidator) {
@SuppressWarnings("unchecked")
GrpcConstraintValidator<MessageLiteOrBuilder> safeConstraintInstance =
(GrpcConstraintValidator<MessageLiteOrBuilder>) 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 <E> type of the gRPC request message
* @return validators to be used in conjunction with the request
*/
<E> List<GrpcConstraintValidator<MessageLiteOrBuilder>> findValidators(E request) {
return validatorMap.values()
.stream()
.filter(cs -> checkForGenericTypeArgument(cs, request))
.collect(Collectors.toList());
}

private <E> boolean checkForGenericTypeArgument(
GrpcConstraintValidator<MessageLiteOrBuilder> grpcConstraintValidator, E request) {

List<Type> 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()));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright (c) 2016-2021 Michael Zhang <yidongnan@gmail.com>
*
* 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 <ReqT, RespT> Listener<ReqT> interceptCall(
ServerCall<ReqT, RespT> call,
Metadata headers,
ServerCallHandler<ReqT, RespT> next) {

Listener<ReqT> delegate = next.startCall(call, headers);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if what i know is right, reason printing call already closed is next.startCall is non thread-safe.

solution : https://groups.google.com/g/grpc-io/c/_osH2D6L9Ck

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what exactly happens, but it's probably not related to mutli-threading, but "owning" the server call instance.
You only close the call, but you never tell the control structure, that you have done so, thus the request processing "continues" as normal. Then the close you have send kicks in and the server tries to properly close the connection, thus sending the close twice. Which is probably the exception you are seeing.

Instead of (or maybe in addition to) closing the call instance, you have to throw a RuntimeStatusException. This will cause the control structure to shutdown with you error status and thus avoids the duplicate close.
(I haven't tested this, just my guess from your one sentence without stacktrace)

I did something similar here:
https://github.com/yidongnan/grpc-spring-boot-starter/blob/master/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/interceptors/ExceptionTranslatingServerInterceptor.java#L44

Alternatively do it like this (This is also from a validation framework, so it might work better for your usecase):
https://github.com/envoyproxy/protoc-gen-validate/blob/5ef93ae28a92362ede78aaa49c5c3e290c70e324/java/pgv-java-grpc/src/main/java/io/envoyproxy/pgv/grpc/ValidatingServerInterceptor.java

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh.. thank you 😄

return new RequestValidationListener<>(delegate, call, headers, grpcValidationResolver);
}

}
Loading