diff --git a/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcSecurityInServletFailureAnalyzer.java b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcSecurityInServletFailureAnalyzer.java new file mode 100644 index 00000000..2ff00711 --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcSecurityInServletFailureAnalyzer.java @@ -0,0 +1,81 @@ +/* + * Copyright 2012-present 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 + * + * https://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.boot.grpc.server.autoconfigure; + +import org.jspecify.annotations.Nullable; + +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.boot.diagnostics.AbstractFailureAnalyzer; +import org.springframework.boot.diagnostics.FailureAnalysis; +import org.springframework.boot.diagnostics.FailureAnalyzer; +import org.springframework.core.env.Environment; +import org.springframework.grpc.server.security.GrpcSecurity; +import org.springframework.util.ClassUtils; + +/** + * {@link FailureAnalyzer} for missing {@link GrpcSecurity} in servlet-based gRPC + * applications. + * + * @author Andrey Litvitski + */ +public class GrpcSecurityInServletFailureAnalyzer extends AbstractFailureAnalyzer { + + private static final String GRPC_SECURITY_PATH = "org.springframework.grpc.server.security.GrpcSecurity"; + + private static final String GRPC_SERVLET_PATH = "io.grpc.servlet.jakarta.GrpcServlet"; + + private final Environment environment; + + public GrpcSecurityInServletFailureAnalyzer(Environment environment) { + this.environment = environment; + } + + @Override + protected @Nullable FailureAnalysis analyze(Throwable rootFailure, NoSuchBeanDefinitionException cause) { + if (!isMissingGrpcSecurity(cause) || !isActuallyServletMode()) { + return null; + } + return new FailureAnalysis(getDescription(), getAction(), cause); + } + + private boolean isMissingGrpcSecurity(NoSuchBeanDefinitionException ex) { + Class type = ex.getBeanType(); + return type != null && GRPC_SECURITY_PATH.equals(type.getName()); + } + + private boolean isActuallyServletMode() { + boolean servletOnClassPath = ClassUtils.isPresent(GRPC_SERVLET_PATH, getClass().getClassLoader()); + if (!servletOnClassPath) { + return false; + } + String servletPropertyName = "spring.grpc.server.servlet.enabled"; + return this.environment.getProperty(servletPropertyName, Boolean.class, true); + } + + private String getDescription() { + return """ + GrpcSecurity is not available because this application is running the gRPC server in servlet mode (GrpcServlet). + """; + } + + private String getAction() { + return """ + Configure security using the standard Spring Security servlet configuration (SecurityFilterChain / HttpSecurity). + """; + } + +} diff --git a/spring-grpc-server-spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-grpc-server-spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index fc5b89e9..f25f566d 100644 --- a/spring-grpc-server-spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-grpc-server-spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -3,3 +3,6 @@ org.springframework.boot.grpc.server.autoconfigure.security.GrpcDisableCsrfHttpC org.springframework.boot.EnvironmentPostProcessor=\ org.springframework.boot.grpc.server.autoconfigure.ServletEnvironmentPostProcessor + +org.springframework.boot.diagnostics.FailureAnalyzer=\ +org.springframework.boot.grpc.server.autoconfigure.GrpcSecurityInServletFailureAnalyzer \ No newline at end of file diff --git a/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcSecurityInServletFailureAnalyzerTests.java b/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcSecurityInServletFailureAnalyzerTests.java new file mode 100644 index 00000000..db181e9d --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcSecurityInServletFailureAnalyzerTests.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012-present 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 + * + * https://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.boot.grpc.server.autoconfigure; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.boot.diagnostics.FailureAnalysis; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.core.env.Environment; +import org.springframework.grpc.server.security.GrpcSecurity; + +/** + * Tests for {@link GrpcSecurityInServletFailureAnalyzer}. + * + * @author Andrey Litvitski + */ +class GrpcSecurityInServletFailureAnalyzerTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner(); + + @Test + void whenServletModeEnabledThenGrpcSecurityFailureIsAnalyzed() { + this.contextRunner.withPropertyValues("spring.grpc.server.servlet.enabled=true").run((context) -> { + Environment environment = context.getEnvironment(); + GrpcSecurityInServletFailureAnalyzer analyzer = new GrpcSecurityInServletFailureAnalyzer(environment); + NoSuchBeanDefinitionException ex = new NoSuchBeanDefinitionException(GrpcSecurity.class); + FailureAnalysis analysis = analyzer.analyze(ex, ex); + assertThat(analysis).isNotNull(); + }); + } + + @Test + void whenServletModeDefaultThenGrpcSecurityFailureIsAnalyzed() { + this.contextRunner.run((context) -> { + Environment environment = context.getEnvironment(); + GrpcSecurityInServletFailureAnalyzer analyzer = new GrpcSecurityInServletFailureAnalyzer(environment); + NoSuchBeanDefinitionException ex = new NoSuchBeanDefinitionException(GrpcSecurity.class); + FailureAnalysis analysis = analyzer.analyze(ex, ex); + assertThat(analysis).isNotNull(); + }); + } + + @Test + void whenServletModeDisabledThenGrpcSecurityFailureIsNotAnalyzed() { + this.contextRunner.withPropertyValues("spring.grpc.server.servlet.enabled=false").run((context) -> { + Environment environment = context.getEnvironment(); + GrpcSecurityInServletFailureAnalyzer analyzer = new GrpcSecurityInServletFailureAnalyzer(environment); + NoSuchBeanDefinitionException ex = new NoSuchBeanDefinitionException(GrpcSecurity.class); + FailureAnalysis analysis = analyzer.analyze(ex, ex); + assertThat(analysis).isNull(); + }); + } + +}