diff --git a/gradle.properties b/gradle.properties index 35e035c9..f5a8b64f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,14 +1,14 @@ grpcVersion=1.76.0 -springBootVersion=3.5.7 -springCloudVersion=2025.0.0 +springBootVersion=4.0.0 +springCloudVersion=2025.1.0 gradleErrorPronePluginVersion=3.0.1 errorProneVersion=2.16 -lombokVersion=1.18.24 +lombokVersion=1.18.38 -version=5.2.1-SNAPSHOT +version=6.0.0-SNAPSHOT group=io.github.lognet description=Spring Boot starter for Google RPC. gitHubUrl=https\://github.com/LogNet/grpc-spring-boot-starter @@ -25,4 +25,4 @@ org.gradle.jvmargs=--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAME --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \ --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \ --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \ - --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED \ No newline at end of file + --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index fce403e4..e69d0402 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/grpc-spring-boot-starter-demo/build.gradle b/grpc-spring-boot-starter-demo/build.gradle index 5fd82226..ad72951d 100644 --- a/grpc-spring-boot-starter-demo/build.gradle +++ b/grpc-spring-boot-starter-demo/build.gradle @@ -1,7 +1,7 @@ import org.lognet.springboot.grpc.gradle.ReactiveFeature buildscript { - ext.kotlin_version = '1.9.10' + ext.kotlin_version = '2.1.21' repositories { mavenCentral() } @@ -80,6 +80,7 @@ dependencies { implementation "org.springframework.security:spring-security-config" implementation "org.springframework.security:spring-security-oauth2-jose" implementation "org.springframework.security:spring-security-oauth2-resource-server" + implementation "org.springframework.boot:spring-boot-security" implementation 'org.springframework.boot:spring-boot-starter-validation' compileOnly 'org.springframework:spring-tx' @@ -89,8 +90,9 @@ dependencies { implementation project(':grpc-spring-boot-starter') implementation project(':grpc-client-spring-boot-starter') - testImplementation 'org.springframework.boot:spring-boot-starter-aop' + testImplementation 'org.springframework.boot:spring-boot-starter-aspectj' testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.boot:spring-boot-resttestclient' testImplementation 'com.github.stefanbirkner:system-rules:1.18.0' testImplementation('org.springframework.cloud:spring-cloud-starter-consul-discovery') testImplementation 'org.awaitility:awaitility:4.0.3' @@ -98,12 +100,15 @@ dependencies { testImplementation "org.springframework.cloud:spring-cloud-config-client" testImplementation "org.springframework.cloud:spring-cloud-starter-bootstrap" - testImplementation "com.playtika.testcontainers:embedded-keycloak:3.1.11" + testImplementation "com.github.dasniko:testcontainers-keycloak:3.7.0" testImplementation "com.playtika.testcontainers:embedded-consul:3.1.11" + testImplementation 'org.springframework.security:spring-security-core' + testImplementation "org.springframework.boot:spring-boot-security" + testImplementation "org.springframework.boot:spring-boot-security-oauth2-resource-server" + testImplementation "org.springframework.boot:spring-boot-validation" testImplementation 'org.hamcrest:hamcrest:2.2' - testImplementation 'org.mockito:mockito-core:2.23.0' pureNettyTestImplementation "io.grpc:grpc-netty" @@ -120,7 +125,6 @@ dependencies { reactiveTestImplementation "com.playtika.testcontainers:embedded-postgresql:3.1.11" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - } configurations.all { @@ -150,10 +154,3 @@ compileTestKotlin { jvmTarget = JavaVersion.VERSION_17 } } - - - - - - - diff --git a/grpc-spring-boot-starter-demo/src/customSecurityTest/java/org/lognet/springboot/grpc/auth/CustomSecurityTest.java b/grpc-spring-boot-starter-demo/src/customSecurityTest/java/org/lognet/springboot/grpc/auth/CustomSecurityTest.java index 98d08066..334dea19 100644 --- a/grpc-spring-boot-starter-demo/src/customSecurityTest/java/org/lognet/springboot/grpc/auth/CustomSecurityTest.java +++ b/grpc-spring-boot-starter-demo/src/customSecurityTest/java/org/lognet/springboot/grpc/auth/CustomSecurityTest.java @@ -40,7 +40,7 @@ public class CustomSecurityTest extends GrpcServerTestBase { public static class DemoGrpcSecurityConfig extends GrpcSecurityConfigurerAdapter { @Override - public void configure(GrpcSecurity builder) throws Exception { + public void configure(GrpcSecurity builder) { builder.authorizeRequests() .withSecuredAnnotation() .authenticationSchemeSelector(scheme -> diff --git a/grpc-spring-boot-starter-demo/src/noConsulDependenciesTest/java/org/lognet/springboot/grpc/simple/Issue295Test.java b/grpc-spring-boot-starter-demo/src/noConsulDependenciesTest/java/org/lognet/springboot/grpc/simple/Issue295Test.java index ab360892..f604ab2e 100644 --- a/grpc-spring-boot-starter-demo/src/noConsulDependenciesTest/java/org/lognet/springboot/grpc/simple/Issue295Test.java +++ b/grpc-spring-boot-starter-demo/src/noConsulDependenciesTest/java/org/lognet/springboot/grpc/simple/Issue295Test.java @@ -5,8 +5,9 @@ import org.lognet.springboot.grpc.GrpcServerTestBase; import org.lognet.springboot.grpc.demo.DemoApp; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.resttestclient.TestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.test.context.ActiveProfiles; @@ -27,6 +28,7 @@ , "spring.main.web-application-type=servlet" }) @ActiveProfiles({"disable-security"}) +@AutoConfigureTestRestTemplate public class Issue295Test extends GrpcServerTestBase { diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/ConfigServerEnvironmentBaseTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/ConfigServerEnvironmentBaseTest.java index d4ef4e71..6e0e3ca1 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/ConfigServerEnvironmentBaseTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/ConfigServerEnvironmentBaseTest.java @@ -4,13 +4,10 @@ import org.junit.ClassRule; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; -import org.lognet.springboot.grpc.autoconfigure.GRpcAutoConfiguration; -import org.lognet.springboot.grpc.autoconfigure.security.SecurityAutoConfiguration; +import org.lognet.springboot.grpc.configserver.ConfigServerTestApplication; import org.lognet.springboot.grpc.demo.DemoApp; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; -import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration; -import org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.env.Environment; @@ -61,14 +58,12 @@ public static void startConfigServer(Properties properties ) throws IOException, } - server = SpringApplication.run(org.springframework.cloud.config.server.ConfigServerApplication.class, + server = SpringApplication.run(ConfigServerTestApplication.class, "--server.port=" + configPort, - "--spring.autoconfigure.exclude="+Stream.of(GRpcAutoConfiguration.class, - OAuth2ResourceServerAutoConfiguration.class, - ManagementWebSecurityAutoConfiguration.class, - SecurityAutoConfiguration.class - ) - .map(Class::getName).collect(Collectors.joining(",")), + "--spring.autoconfigure.exclude="+Stream.of( + "org.springframework.boot.security.oauth2.server.resource.autoconfigure.OAuth2ResourceServerAutoConfiguration", + "org.springframework.boot.security.autoconfigure.actuate.web.servlet.ManagementWebSecurityAutoConfiguration" + ).collect(Collectors.joining(",")), "--spring.cloud.consul.discovery.enabled=false", "--spring.cloud.service-registry.enabled=false", "--spring.cloud.service-registry.auto-registration.enabled=false", diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/CustomInterceptorsOrderTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/CustomInterceptorsOrderTest.java index 2cde2fec..9bef9144 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/CustomInterceptorsOrderTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/CustomInterceptorsOrderTest.java @@ -147,7 +147,7 @@ public UserDetails user() { @Override - public void configure(GrpcSecurity builder) throws Exception { + public void configure(GrpcSecurity builder) { final UserDetails user = builder.getApplicationContext().getBean(UserDetails.class); diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/GRpcServerBuilderConfigurerTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/GRpcServerBuilderConfigurerTest.java index 376fb4ba..2f4bc9ab 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/GRpcServerBuilderConfigurerTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/GRpcServerBuilderConfigurerTest.java @@ -12,7 +12,7 @@ import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.boot.test.system.OutputCaptureRule; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringRunner; @@ -37,7 +37,7 @@ public class GRpcServerBuilderConfigurerTest { @Autowired private GRpcServerBuilderConfigurer customGrpcServerBuilderConfigurer; - @MockBean(name = "anotherConfigurer") + @MockitoBean(name = "anotherConfigurer") private GRpcServerBuilderConfigurer anotherConfigurer; @Rule diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/GrpcMeterTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/GrpcMeterTest.java index 90e6a7ba..1e395728 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/GrpcMeterTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/GrpcMeterTest.java @@ -23,7 +23,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.test.context.ActiveProfiles; @@ -100,7 +100,7 @@ public Iterable getTags(Status status, MethodDescriptor methodDescrip } } - @SpyBean + @MockitoSpyBean private RequestAwareGRpcMetricsTagsContributor shouldNotBeInvoked; @Autowired diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/ValidationTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/ValidationTest.java index 44d69b98..14915511 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/ValidationTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/ValidationTest.java @@ -23,7 +23,7 @@ import org.lognet.springboot.grpc.demo.DemoApp; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.test.context.ActiveProfiles; @@ -46,10 +46,15 @@ public Status handle(Object message, Status status, Exception exception, Metadat } }; } + + @Bean + public HalfCloseInterceptor halfCloseInterceptor() { + return new HalfCloseInterceptor(); + } } private GreeterGrpc.GreeterBlockingStub stub; - @SpyBean + @MockitoSpyBean HalfCloseInterceptor halfCloseInterceptor; private static Locale systemDefaultLocale; diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/actuator/ActuatorTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/actuator/ActuatorTest.java index 689fdc75..c2d5840a 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/actuator/ActuatorTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/actuator/ActuatorTest.java @@ -1,7 +1,5 @@ package org.lognet.springboot.grpc.actuator; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.jayway.jsonpath.Configuration; import com.jayway.jsonpath.DocumentContext; import com.jayway.jsonpath.JsonPath; @@ -17,23 +15,23 @@ import org.lognet.springboot.grpc.TestConfig; import org.lognet.springboot.grpc.demo.DemoApp; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.actuate.health.Status; +import org.springframework.boot.health.contributor.Status; +import org.springframework.boot.resttestclient.TestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringRunner; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.node.ObjectNode; import java.time.Duration; import java.util.Optional; import java.util.Set; -import java.util.Spliterator; -import java.util.Spliterators; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.stream.Stream; -import java.util.stream.StreamSupport; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; @@ -51,6 +49,7 @@ , "spring.main.web-application-type=servlet" }) @ActiveProfiles({"disable-security", "measure"}) +@AutoConfigureTestRestTemplate public class ActuatorTest extends GrpcServerTestBase { @Autowired @@ -112,8 +111,8 @@ protected void afterGreeting() throws Exception { ResponseEntity metricsResponse = restTemplate.getForEntity("/actuator/metrics", ObjectNode.class); assertEquals(HttpStatus.OK, metricsResponse.getStatusCode()); final String metricName = "grpc.server.calls"; - final Optional containsGrpcServerCallsMetric = StreamSupport.stream(Spliterators.spliteratorUnknownSize(metricsResponse.getBody().withArray("names") - .elements(), Spliterator.NONNULL), false) + final Optional containsGrpcServerCallsMetric = metricsResponse.getBody().withArray("names") + .elements().stream() .map(JsonNode::asText) .filter(metricName::equals) .findFirst(); diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/AllAuthConfigTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/AllAuthConfigTest.java index 2c289ce0..828d422d 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/AllAuthConfigTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/AllAuthConfigTest.java @@ -14,6 +14,7 @@ import org.springframework.context.annotation.Import; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; import static org.junit.Assert.assertNotNull; @@ -24,12 +25,13 @@ @ActiveProfiles("keycloack-test") @RunWith(SpringRunner.class) @Import({AllAuthConfigTest.TestCfg.class}) +@ContextConfiguration(initializers = KeycloakContainerInitializer.class) public class AllAuthConfigTest extends JwtAuthBaseTest { @TestConfiguration static class TestCfg extends GrpcSecurityConfigurerAdapter { @Override - public void configure(GrpcSecurity builder) throws Exception { + public void configure(GrpcSecurity builder) { builder.authorizeRequests() .anyMethod().authenticated() diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/ConcurrentAuthConfigTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/ConcurrentAuthConfigTest.java index 0943ec44..b2b045f4 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/ConcurrentAuthConfigTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/ConcurrentAuthConfigTest.java @@ -55,10 +55,9 @@ public class ConcurrentAuthConfigTest extends GrpcServerTestBase { static class TestCfg extends GrpcSecurityConfigurerAdapter { @Override - public void configure(GrpcSecurity builder) throws Exception { - DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + public void configure(GrpcSecurity builder) { UserDetailsService users = new InMemoryUserDetailsManager(user1, user2); - provider.setUserDetailsService(users); + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(users); provider.setPasswordEncoder(NoOpPasswordEncoder.getInstance()); builder diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/DefaultAuthConfigTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/DefaultAuthConfigTest.java index e396f8e1..98e5c57d 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/DefaultAuthConfigTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/DefaultAuthConfigTest.java @@ -9,6 +9,7 @@ import org.lognet.springboot.grpc.demo.DemoApp; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; import static org.junit.Assert.assertNotNull; @@ -18,6 +19,7 @@ @SpringBootTest(classes = DemoApp.class) @ActiveProfiles("keycloack-test") @RunWith(SpringRunner.class) +@ContextConfiguration(initializers = KeycloakContainerInitializer.class) public class DefaultAuthConfigTest extends JwtAuthBaseTest { diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/DisabledSecuredAnnTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/DisabledSecuredAnnTest.java index fa31e87c..4ed8a068 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/DisabledSecuredAnnTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/DisabledSecuredAnnTest.java @@ -26,7 +26,7 @@ public class DisabledSecuredAnnTest extends JwtAuthBaseTest { @TestConfiguration static class TestCfg extends GrpcSecurityConfigurerAdapter { @Override - public void configure(GrpcSecurity builder) throws Exception { + public void configure(GrpcSecurity builder) { builder.authorizeRequests().withoutSecuredAnnotation(); } } diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/ExcludeMethodAuthTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/ExcludeMethodAuthTest.java index 137dece3..6d523043 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/ExcludeMethodAuthTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/ExcludeMethodAuthTest.java @@ -83,7 +83,7 @@ public static UserDetails admin() { @Override - public void configure(GrpcSecurity builder) throws Exception { + public void configure(GrpcSecurity builder) { builder .authorizeRequests() .methods(CustomServiceGrpc.getCustomMethod()).hasAnyRole("admin") diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/FailLateSecurityInterceptorTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/FailLateSecurityInterceptorTest.java index d6a388ae..8f9de4d7 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/FailLateSecurityInterceptorTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/FailLateSecurityInterceptorTest.java @@ -17,7 +17,10 @@ import org.lognet.springboot.grpc.HalfCloseInterceptor; import org.lognet.springboot.grpc.demo.DemoApp; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import org.springframework.test.context.junit4.SpringRunner; @SpringBootTest( @@ -25,8 +28,18 @@ properties = "grpc.security.auth.fail-fast=false" ) @RunWith(SpringRunner.class) +@Import(FailLateSecurityInterceptorTest.TestCfg.class) public class FailLateSecurityInterceptorTest extends GrpcServerTestBase { - @SpyBean + + @TestConfiguration + static class TestCfg { + @Bean + public HalfCloseInterceptor halfCloseInterceptor() { + return new HalfCloseInterceptor(); + } + } + + @MockitoSpyBean HalfCloseInterceptor halfCloseInterceptor; @Test diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/FailedAuthGrpcSecurityConfig.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/FailedAuthGrpcSecurityConfig.java index 950a583d..255d0cb7 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/FailedAuthGrpcSecurityConfig.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/FailedAuthGrpcSecurityConfig.java @@ -10,7 +10,7 @@ public class FailedAuthGrpcSecurityConfig extends GrpcSecurityConfigurerAdapter { @Override - public void configure(GrpcSecurity builder) throws Exception { + public void configure(GrpcSecurity builder) { builder.authorizeRequests() .anyMethod().authenticated() diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/JwtAuthBaseTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/JwtAuthBaseTest.java index 37314d8d..6a842d10 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/JwtAuthBaseTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/JwtAuthBaseTest.java @@ -1,26 +1,20 @@ package org.lognet.springboot.grpc.auth; - import io.grpc.Channel; import io.grpc.ClientInterceptors; import lombok.extern.slf4j.Slf4j; import org.lognet.springboot.grpc.GrpcServerTestBase; import org.lognet.springboot.grpc.security.AuthClientInterceptor; import org.lognet.springboot.grpc.security.AuthHeader; -import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Value; -import org.springframework.cloud.config.client.RetryProperties; -import org.springframework.cloud.config.client.RetryTemplateFactory; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.retry.support.RetryTemplate; -import org.springframework.retry.support.RetryTemplateBuilder; import org.springframework.util.LinkedMultiValueMap; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.DefaultUriBuilderFactory; -import org.testcontainers.shaded.com.fasterxml.jackson.core.JsonProcessingException; import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper; import java.nio.ByteBuffer; @@ -46,18 +40,15 @@ public JwtAuthBaseTest() { @Override protected Channel getChannel() { return getChannel(globalSecuredChannel); - } protected Channel getChannel(boolean authenticated) { return authenticated ? ClientInterceptors.intercept(super.getChannel(), getAuthClientInterceptor()) : super.getChannel(); - } protected final static String USER_NAME = "keycloak-test"; - protected AuthClientInterceptor getAuthClientInterceptor() { return new AuthClientInterceptor( AuthHeader.builder().bearer().tokenSupplier(this::generateToken)); @@ -70,38 +61,35 @@ protected ByteBuffer generateToken() { final LinkedMultiValueMap req = new LinkedMultiValueMap<>(); - req.add("client_id", "any-client"); req.add("client_secret", "08f64721-7fef-4d8b-a0fc-8f940a621451"); req.add("grant_type", "password"); req.add("username", USER_NAME); req.add("password", "123Start!"); - HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); final RestTemplate restTemplate = new RestTemplate(); restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory(authServerUrl)); - - - try { final ResponseEntity response = RetryTemplate.builder() - .exponentialBackoff(300,1.5,3000) + .maxAttempts(10) + .exponentialBackoff(500, 2, 5000) .build() .execute(ctx -> { Optional.ofNullable(ctx.getLastThrowable()) .ifPresent(e -> log.info("Retrying on ...", e)); return restTemplate - .postForEntity("/realms/test-realm/protocol/openid-connect/token", new HttpEntity<>(req, headers), String.class); + .postForEntity("/realms/test-realm/protocol/openid-connect/token", + new HttpEntity<>(req, headers), String.class); }); return ByteBuffer.wrap(new ObjectMapper().readTree(response.getBody()) .at("/access_token") .asText().getBytes()); } catch (Exception e) { - log.error("Failed to generate token",e ); + log.error("Failed to generate token", e); throw new RuntimeException(e); } } diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/JwtAuthorityTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/JwtAuthorityTest.java index eb0eaa7d..c949f0dc 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/JwtAuthorityTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/JwtAuthorityTest.java @@ -13,6 +13,7 @@ import org.springframework.context.annotation.Import; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; @@ -20,6 +21,7 @@ @ActiveProfiles("keycloack-test") @RunWith(SpringRunner.class) @Import({JwtAuthorityTest.TestCfg.class}) +@ContextConfiguration(initializers = KeycloakContainerInitializer.class) public class JwtAuthorityTest extends JwtAuthBaseTest { @@ -30,7 +32,7 @@ static class TestCfg extends GrpcSecurityConfigurerAdapter { private JwtDecoder jwtDecoder; @Override - public void configure(GrpcSecurity builder) throws Exception { + public void configure(GrpcSecurity builder) { super.configure(builder); builder.authorizeRequests() diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/JwtRoleTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/JwtRoleTest.java index 34852372..afe597f7 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/JwtRoleTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/JwtRoleTest.java @@ -22,6 +22,7 @@ import org.springframework.context.annotation.Import; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; import java.util.List; @@ -44,6 +45,7 @@ @ActiveProfiles("keycloack-test") @RunWith(SpringRunner.class) @Import({JwtRoleTest.TestCfg.class}) +@ContextConfiguration(initializers = KeycloakContainerInitializer.class) public class JwtRoleTest extends JwtAuthBaseTest { @@ -70,7 +72,7 @@ private static class DemoGrpcSecurityAdapter extends GrpcSecurityConfigurerAdapt private JwtDecoder jwtDecoder; @Override - public void configure(GrpcSecurity builder) throws Exception { + public void configure(GrpcSecurity builder) { builder.authorizeRequests() .methods(GreeterGrpc.getSayHelloMethod()).hasAnyRole("reader") .methods(CalculatorGrpc.getCalculateMethod()).hasAnyRole("anotherRole") diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/KeycloakContainerInitializer.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/KeycloakContainerInitializer.java new file mode 100644 index 00000000..560eabc9 --- /dev/null +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/KeycloakContainerInitializer.java @@ -0,0 +1,42 @@ +package org.lognet.springboot.grpc.auth; + +import dasniko.testcontainers.keycloak.KeycloakContainer; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.test.context.support.TestPropertySourceUtils; +import org.testcontainers.containers.wait.strategy.Wait; + +import java.time.Duration; + +public class KeycloakContainerInitializer + implements ApplicationContextInitializer { + + private static final KeycloakContainer KEYCLOAK; + + static { + KEYCLOAK = new KeycloakContainer() + .waitingFor(Wait.forHttp("/realms/master") + .forPort(8080) + .forStatusCode(200) + .withStartupTimeout(Duration.ofMinutes(3))) + .withStartupTimeout(Duration.ofMinutes(3)) + .withRealmImportFile("test-realm-realm.json"); + KEYCLOAK.start(); + } + + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + String authServerUrl = KEYCLOAK.getAuthServerUrl(); + if (!authServerUrl.endsWith("/")) { + authServerUrl = authServerUrl + "/"; + } + TestPropertySourceUtils.addInlinedPropertiesToEnvironment(applicationContext, + "embedded.keycloak.auth-server-url=" + authServerUrl, + "spring.security.oauth2.resourceserver.jwt.issuer-uri=" + authServerUrl + "realms/test-realm" + ); + } + + public static KeycloakContainer getContainer() { + return KEYCLOAK; + } +} diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/PerCallDefaultAuthConfigTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/PerCallDefaultAuthConfigTest.java index 5df82942..cba686f8 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/PerCallDefaultAuthConfigTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/PerCallDefaultAuthConfigTest.java @@ -12,6 +12,7 @@ import org.lognet.springboot.grpc.security.AuthHeader; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; import static org.junit.Assert.assertNotNull; @@ -22,7 +23,7 @@ @SpringBootTest(classes = DemoApp.class) @ActiveProfiles("keycloack-test") @RunWith(SpringRunner.class) - +@ContextConfiguration(initializers = KeycloakContainerInitializer.class) public class PerCallDefaultAuthConfigTest extends JwtAuthBaseTest { private AuthCallCredentials callCredentials; diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/PrePostSecurityAuthTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/PrePostSecurityAuthTest.java index 63e1931b..15acf32f 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/PrePostSecurityAuthTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/PrePostSecurityAuthTest.java @@ -26,8 +26,8 @@ import org.mockito.verification.VerificationMode; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.security.access.prepost.PreAuthorize; @@ -110,7 +110,7 @@ public PermissionService permissionService(){ } @Override - public void configure(GrpcSecurity builder) throws Exception { + public void configure(GrpcSecurity builder) { builder.authorizeRequests() .withSecuredAnnotation() .userDetailsService(new InMemoryUserDetailsManager( @@ -150,10 +150,10 @@ public void configure(GrpcSecurity builder) throws Exception { .setDescription("Keep the ring") .build(); - @MockBean + @MockitoBean private ITaskService service; - @SpyBean + @MockitoSpyBean private PermissionService permissionService; @Test @@ -299,7 +299,7 @@ public void bidiStreamPreAuthorizeFailCallTest() { observer.get(Duration.ofSeconds(10)); }); assertThat(statusRuntimeException.getStatus().getCode(), Matchers.is(Status.Code.PERMISSION_DENIED)); - Mockito.verifyZeroInteractions(service); + Mockito.verifyNoInteractions(service); } diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/SecurityInterceptorTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/SecurityInterceptorTest.java index d6cfc2ff..e1a883d9 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/SecurityInterceptorTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/SecurityInterceptorTest.java @@ -23,7 +23,6 @@ import org.lognet.springboot.grpc.security.GrpcSecurityConfigurerAdapter; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.security.core.userdetails.User; @@ -37,7 +36,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.verifyNoInteractions; @SpringBootTest(classes = DemoApp.class) @RunWith(SpringRunner.class) @@ -47,7 +46,7 @@ public class SecurityInterceptorTest extends GrpcServerTestBase { @TestConfiguration static class TestCfg extends GrpcSecurityConfigurerAdapter { @Override - public void configure(GrpcSecurity builder) throws Exception { + public void configure(GrpcSecurity builder) { builder.authorizeRequests() .withSecuredAnnotation() .userDetailsService(new InMemoryUserDetailsManager( diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/UserDetailsAuthTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/UserDetailsAuthTest.java index 93f0ef31..09338ff5 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/UserDetailsAuthTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/UserDetailsAuthTest.java @@ -81,7 +81,7 @@ public static UserDetails user() { @Override - public void configure(GrpcSecurity builder) throws Exception { + public void configure(GrpcSecurity builder) { builder.authorizeRequests() .methods(GreeterGrpc.getSayHelloMethod()).hasAnyRole("reader") .methods(GreeterGrpc.getSayAuthOnlyHelloMethod()).hasAnyRole("reader") diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/configserver/ConfigServerTestApplication.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/configserver/ConfigServerTestApplication.java new file mode 100644 index 00000000..ff1bbdcd --- /dev/null +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/configserver/ConfigServerTestApplication.java @@ -0,0 +1,9 @@ +package org.lognet.springboot.grpc.configserver; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.config.server.EnableConfigServer; + +@SpringBootApplication +@EnableConfigServer +public class ConfigServerTestApplication { +} diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/consul/ConsulDefaultRegistrationTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/consul/ConsulDefaultRegistrationTest.java index 5af8370c..6f3461b8 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/consul/ConsulDefaultRegistrationTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/consul/ConsulDefaultRegistrationTest.java @@ -1,7 +1,7 @@ package org.lognet.springboot.grpc.consul; -import com.ecwid.consul.v1.health.model.Check; -import com.ecwid.consul.v1.health.model.HealthService; +import org.springframework.cloud.consul.model.http.health.Check; +import org.springframework.cloud.consul.model.http.health.HealthService; import org.hamcrest.Matchers; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/consul/ConsulNoopHealthCheckTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/consul/ConsulNoopHealthCheckTest.java index a26d25b3..6c316f35 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/consul/ConsulNoopHealthCheckTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/consul/ConsulNoopHealthCheckTest.java @@ -1,6 +1,6 @@ package org.lognet.springboot.grpc.consul; -import com.ecwid.consul.v1.health.model.HealthService; +import org.springframework.cloud.consul.model.http.health.HealthService; import org.hamcrest.Matchers; import org.junit.runner.RunWith; import org.lognet.springboot.grpc.demo.DemoApp; diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/consul/ConsulPerServiceHealthCheckTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/consul/ConsulPerServiceHealthCheckTest.java index 245d5489..2c0f14ab 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/consul/ConsulPerServiceHealthCheckTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/consul/ConsulPerServiceHealthCheckTest.java @@ -1,7 +1,7 @@ package org.lognet.springboot.grpc.consul; -import com.ecwid.consul.v1.health.model.Check; -import com.ecwid.consul.v1.health.model.HealthService; +import org.springframework.cloud.consul.model.http.health.Check; +import org.springframework.cloud.consul.model.http.health.HealthService; import org.hamcrest.Matchers; import org.junit.runner.RunWith; import org.lognet.springboot.grpc.demo.DemoApp; diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/consul/ConsulRegistrationBaseTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/consul/ConsulRegistrationBaseTest.java index 45cb289e..8c673f50 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/consul/ConsulRegistrationBaseTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/consul/ConsulRegistrationBaseTest.java @@ -1,10 +1,8 @@ package org.lognet.springboot.grpc.consul; -import com.ecwid.consul.v1.ConsulClient; -import com.ecwid.consul.v1.QueryParams; -import com.ecwid.consul.v1.health.HealthServicesRequest; -import com.ecwid.consul.v1.health.model.HealthService; +import org.springframework.cloud.consul.ConsulClient; +import org.springframework.cloud.consul.model.http.health.HealthService; import io.grpc.BindableService; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; @@ -106,11 +104,8 @@ public void contextLoads() { final List healthServices = Awaitility.await() .atMost(Duration.ofMinutes(1)) .pollInterval(Duration.ofSeconds(3)) - .until(() -> consulClient.getHealthServices(serviceId, HealthServicesRequest.newBuilder() - .setPassing(true) - .setQueryParams(QueryParams.DEFAULT) - .build()) - .getValue() + .until(() -> consulClient.getHealthServices(serviceId, true, null, null, ConsulClient.QueryParams.DEFAULT) + .getBody() , Matchers.hasSize(Matchers.greaterThanOrEqualTo(minExpectedRegistrations))); diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/consul/ConsulStandaloneServicesHealthCheckTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/consul/ConsulStandaloneServicesHealthCheckTest.java index f6847eeb..32ae81ef 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/consul/ConsulStandaloneServicesHealthCheckTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/consul/ConsulStandaloneServicesHealthCheckTest.java @@ -1,7 +1,7 @@ package org.lognet.springboot.grpc.consul; -import com.ecwid.consul.v1.health.model.Check; -import com.ecwid.consul.v1.health.model.HealthService; +import org.springframework.cloud.consul.model.http.health.Check; +import org.springframework.cloud.consul.model.http.health.HealthService; import org.hamcrest.Matchers; import org.junit.runner.RunWith; import org.lognet.springboot.grpc.demo.DemoApp; diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/demo/AopServiceMonitor.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/demo/AopServiceMonitor.java index 8100c069..cfa3545c 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/demo/AopServiceMonitor.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/demo/AopServiceMonitor.java @@ -12,10 +12,10 @@ * Created by 310242212 on 01-Nov-16. */ +@Slf4j @Aspect @Component @Profile(value = {"aopTest"}) -@Slf4j public class AopServiceMonitor { public AopServiceMonitor() { diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/demo/routeguide/RouteGuideDemo.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/demo/routeguide/RouteGuideDemo.java index 803445d3..2b9178ad 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/demo/routeguide/RouteGuideDemo.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/demo/routeguide/RouteGuideDemo.java @@ -5,7 +5,6 @@ import io.grpc.examples.routeguide.RouteGuideGrpc; import io.grpc.examples.routeguide.RouteNote; import io.grpc.stub.StreamObserver; -import lombok.extern.slf4j.Slf4j; import org.junit.Test; import org.junit.runner.RunWith; import org.lognet.springboot.grpc.GrpcServerTestBase; @@ -15,7 +14,6 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.util.concurrent.ListenableFuture; import static org.hamcrest.MatcherAssert.assertThat; @@ -25,6 +23,7 @@ import static org.mockito.Mockito.any; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; +import lombok.extern.slf4j.Slf4j; import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE; import java.util.concurrent.CompletableFuture; @@ -34,11 +33,12 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -@Slf4j @RunWith(SpringRunner.class) @SpringBootTest(classes = DemoApp.class, webEnvironment = NONE) @ActiveProfiles("disable-security") +@Slf4j public class RouteGuideDemo extends GrpcServerTestBase { + @Test public void bidirectionalStreamingDemo() throws ExecutionException, InterruptedException, TimeoutException { final RouteGuideGrpc.RouteGuideStub asyncStub = RouteGuideGrpc.newStub(getChannel()); diff --git a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/health/CustomManagedHealthStatusServiceTest.java b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/health/CustomManagedHealthStatusServiceTest.java index 34f9b46a..a4b6ec64 100644 --- a/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/health/CustomManagedHealthStatusServiceTest.java +++ b/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/health/CustomManagedHealthStatusServiceTest.java @@ -12,7 +12,8 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.hamcrest.Matchers; -import org.junit.Rule; +import org.junit.After; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.lognet.springboot.grpc.GRpcService; @@ -21,7 +22,7 @@ import org.mockito.Mockito; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import org.springframework.cloud.commons.util.InetUtils; import org.springframework.cloud.commons.util.InetUtilsProperties; import org.springframework.test.annotation.DirtiesContext; @@ -52,9 +53,18 @@ @Slf4j public class CustomManagedHealthStatusServiceTest extends GrpcServerTestBase { - @Rule public GrpcHealthProbeContainer grpcHealthProbe = new GrpcHealthProbeContainer(); + @Before + public void startHealthProbeContainer() { + grpcHealthProbe.start(); + } + + @After + public void stopHealthProbeContainer() { + grpcHealthProbe.stop(); + } + @TestConfiguration static class Cfg { @GRpcService @@ -76,7 +86,7 @@ public void check(HealthCheckRequest request, StreamObserver { log.info("Shutting down gRPC server ..."); healthStatusManager.ifPresent(ManagedHealthStatusService::onShutdown); + healthStatusManager = Optional.empty(); s.shutdown(); int shutdownGrace = gRpcServerProperties.getShutdownGrace(); diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/GRpcAutoConfiguration.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/GRpcAutoConfiguration.java index 279259d4..3951d6e7 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/GRpcAutoConfiguration.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/GRpcAutoConfiguration.java @@ -21,7 +21,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; +import org.springframework.boot.validation.autoconfigure.ValidationAutoConfiguration; import org.springframework.boot.context.properties.ConfigurationPropertiesBindHandlerAdvisor; import org.springframework.boot.context.properties.ConfigurationPropertiesBinding; import org.springframework.boot.context.properties.bind.AbstractBindHandler; diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/actuate/GRpcActuateAutoConfiguration.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/actuate/GRpcActuateAutoConfiguration.java index f08e7df5..fa15b1b8 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/actuate/GRpcActuateAutoConfiguration.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/actuate/GRpcActuateAutoConfiguration.java @@ -14,18 +14,18 @@ import org.lognet.springboot.grpc.context.LocalRunningGrpcPort; import org.lognet.springboot.grpc.health.ManagedHealthStatusService; import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; -import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; -import org.springframework.boot.actuate.health.AbstractHealthIndicator; -import org.springframework.boot.actuate.health.CompositeHealthContributor; -import org.springframework.boot.actuate.health.Health; -import org.springframework.boot.actuate.health.HealthContributor; -import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.health.autoconfigure.contributor.ConditionalOnEnabledHealthIndicator; +import org.springframework.boot.health.contributor.AbstractHealthIndicator; +import org.springframework.boot.health.contributor.CompositeHealthContributor; +import org.springframework.boot.health.contributor.Health; +import org.springframework.boot.health.contributor.HealthContributor; +import org.springframework.boot.health.contributor.HealthIndicator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.event.EventListener; @@ -44,6 +44,7 @@ public class GRpcActuateAutoConfiguration { @Configuration(proxyBeanMethods = false) @ConditionalOnEnabledHealthIndicator("grpc") + @ConditionalOnBean(GRpcServicesRegistry.class) static class GRpcHealthHealthContributorConfiguration { @Bean @ConditionalOnMissingBean(name = "grpcHealthIndicator") @@ -85,6 +86,7 @@ protected void doHealthCheck(Health.Builder builder) throws Exception { @Configuration(proxyBeanMethods = false) @ConditionalOnAvailableEndpoint(endpoint = GrpcEndpoint.class) + @ConditionalOnBean(GRpcServicesRegistry.class) static class GrpcEndpointConfiguration { @Bean diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/consul/ServiceRegistrationMode.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/consul/ServiceRegistrationMode.java index 32c26545..f9837348 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/consul/ServiceRegistrationMode.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/consul/ServiceRegistrationMode.java @@ -1,11 +1,11 @@ package org.lognet.springboot.grpc.autoconfigure.consul; -import com.ecwid.consul.v1.agent.model.NewService; import io.grpc.Server; import io.grpc.ServerServiceDefinition; import io.grpc.health.v1.HealthGrpc; import org.lognet.springboot.grpc.autoconfigure.GRpcServerProperties; import org.springframework.cloud.consul.discovery.ConsulDiscoveryProperties; +import org.springframework.cloud.consul.model.http.agent.NewService; import org.springframework.cloud.consul.serviceregistry.ConsulAutoRegistration; import org.springframework.context.ApplicationContext; import org.springframework.util.CollectionUtils; diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/consul/ServiceRegistrationStrategy.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/consul/ServiceRegistrationStrategy.java index f454fe3f..ce6cb77a 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/consul/ServiceRegistrationStrategy.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/consul/ServiceRegistrationStrategy.java @@ -1,8 +1,8 @@ package org.lognet.springboot.grpc.autoconfigure.consul; import com.ecwid.consul.json.GsonFactory; -import com.ecwid.consul.v1.agent.model.NewService; import io.grpc.Server; +import org.springframework.cloud.consul.model.http.agent.NewService; import org.springframework.context.ApplicationContext; import java.util.Collection; diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/metrics/GRpcMetricsAutoConfiguration.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/metrics/GRpcMetricsAutoConfiguration.java index 6639e1d0..37f65507 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/metrics/GRpcMetricsAutoConfiguration.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/metrics/GRpcMetricsAutoConfiguration.java @@ -22,8 +22,8 @@ import org.lognet.springboot.grpc.autoconfigure.GRpcAutoConfiguration; import org.lognet.springboot.grpc.autoconfigure.GRpcServerProperties; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; -import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; +import org.springframework.boot.micrometer.metrics.autoconfigure.CompositeMeterRegistryAutoConfiguration; +import org.springframework.boot.micrometer.metrics.autoconfigure.MetricsAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.AllNestedConditions; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/security/BearerTokenSecurityAutoConfiguration.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/security/BearerTokenSecurityAutoConfiguration.java new file mode 100644 index 00000000..f334eade --- /dev/null +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/security/BearerTokenSecurityAutoConfiguration.java @@ -0,0 +1,17 @@ +package org.lognet.springboot.grpc.autoconfigure.security; + +import org.lognet.springboot.grpc.security.BearerTokenAuthSchemeSelector; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.oauth2.core.OAuth2Error; + +@Configuration +@ConditionalOnClass(OAuth2Error.class) +public class BearerTokenSecurityAutoConfiguration { + + @Bean + public BearerTokenAuthSchemeSelector bearerTokenAuthSchemeSelector() { + return new BearerTokenAuthSchemeSelector(); + } +} diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/AuthenticatedAttributeVoter.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/AuthenticatedAttributeVoter.java index 53d6c99d..7560026f 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/AuthenticatedAttributeVoter.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/AuthenticatedAttributeVoter.java @@ -1,24 +1,13 @@ package org.lognet.springboot.grpc.security; -import org.springframework.security.access.AccessDecisionVoter; -import org.springframework.security.access.ConfigAttribute; -import org.springframework.security.core.Authentication; - -import java.util.Collection; - -public class AuthenticatedAttributeVoter implements AccessDecisionVoter { - @Override - public boolean supports(ConfigAttribute attribute) { - return AuthenticatedConfigAttribute.class.isInstance(attribute); - } - - @Override - public boolean supports(Class clazz) { - return true; - } - - @Override - public int vote(Authentication authentication, Object object, Collection attributes) { - return (authentication.isAuthenticated() && attributes.stream().anyMatch(this::supports))? ACCESS_GRANTED:ACCESS_ABSTAIN; - } +/** + * @deprecated No longer used. Retained for binary compatibility only. + * Was removed when migrating from Spring Security legacy access-control + * ({@code ConfigAttribute}/{@code AccessDecisionVoter}) to + * {@code AuthorizationManager}-based infrastructure in Spring Security 6+/7. + * Authentication-based checks are now performed by + * {@link org.springframework.security.authorization.AuthenticatedAuthorizationManager}. + */ +@Deprecated +public class AuthenticatedAttributeVoter { } diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/AuthenticatedConfigAttribute.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/AuthenticatedConfigAttribute.java index 09916b61..daadb36e 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/AuthenticatedConfigAttribute.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/AuthenticatedConfigAttribute.java @@ -1,10 +1,11 @@ package org.lognet.springboot.grpc.security; -import org.springframework.security.access.ConfigAttribute; - -class AuthenticatedConfigAttribute implements ConfigAttribute { - @Override - public String getAttribute() { - return null; - } +/** + * @deprecated No longer used. Retained for binary compatibility only. + * Was removed when migrating from Spring Security legacy access-control + * ({@code ConfigAttribute}/{@code AccessDecisionVoter}) to + * {@code AuthorizationManager}-based infrastructure in Spring Security 6+/7. + */ +@Deprecated +class AuthenticatedConfigAttribute { } diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/BearerTokenAuthSchemeSelector.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/BearerTokenAuthSchemeSelector.java index 3886812d..f38ecb65 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/BearerTokenAuthSchemeSelector.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/BearerTokenAuthSchemeSelector.java @@ -2,8 +2,8 @@ import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; -import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; import org.springframework.security.oauth2.server.resource.BearerTokenError; +import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken; import java.util.Optional; import java.util.regex.Matcher; diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurity.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurity.java index 75ac9312..1f96aa26 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurity.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurity.java @@ -2,61 +2,49 @@ import io.grpc.Context; import io.grpc.ServerInterceptor; -import org.aopalliance.intercept.MethodInvocation; import org.lognet.springboot.grpc.GRpcServicesRegistry; import org.lognet.springboot.grpc.autoconfigure.GRpcServerProperties; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; -import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; -import org.springframework.security.access.expression.method.ExpressionBasedAnnotationAttributeFactory; -import org.springframework.security.access.expression.method.ExpressionBasedPostInvocationAdvice; -import org.springframework.security.access.expression.method.ExpressionBasedPreInvocationAdvice; -import org.springframework.security.access.intercept.AfterInvocationManager; -import org.springframework.security.access.intercept.AfterInvocationProviderManager; -import org.springframework.security.access.method.DelegatingMethodSecurityMetadataSource; -import org.springframework.security.access.prepost.PostInvocationAdviceProvider; -import org.springframework.security.access.prepost.PreInvocationAuthorizationAdviceVoter; -import org.springframework.security.access.prepost.PrePostAnnotationSecurityMetadataSource; -import org.springframework.security.access.vote.AffirmativeBased; -import org.springframework.security.access.vote.RoleVoter; import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authorization.method.PostAuthorizeAuthorizationManager; +import org.springframework.security.authorization.method.PreAuthorizeAuthorizationManager; import org.springframework.security.config.ObjectPostProcessor; import org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder; import org.springframework.security.config.annotation.SecurityBuilder; -import org.springframework.security.config.annotation.SecurityConfigurerAdapter; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.core.Authentication; import org.springframework.security.core.userdetails.UserDetailsService; -import java.util.Arrays; -import java.util.Collection; import java.util.Optional; public class GrpcSecurity extends AbstractConfiguredSecurityBuilder implements SecurityBuilder, ApplicationContextAware { - private ApplicationContext applicationContext; + private ApplicationContext applicationContext; + + public static final Context.Key AUTHENTICATION_CONTEXT_KEY = Context.key("AUTHENTICATION"); - public static final Context.Key AUTHENTICATION_CONTEXT_KEY = Context.key("AUTHENTICATION"); public GrpcSecurity(ObjectPostProcessor objectPostProcessor) { super(objectPostProcessor); - } - public GrpcServiceAuthorizationConfigurer.Registry authorizeRequests() - throws Exception { - - return getOrApply(new GrpcServiceAuthorizationConfigurer (applicationContext.getBean(GRpcServicesRegistry.class))) - .getRegistry(); + public GrpcServiceAuthorizationConfigurer.Registry authorizeRequests() { + GrpcServiceAuthorizationConfigurer configurer = getConfigurer(GrpcServiceAuthorizationConfigurer.class); + if (configurer == null) { + configurer = new GrpcServiceAuthorizationConfigurer(applicationContext.getBean(GRpcServicesRegistry.class)); + with(configurer); + } + return configurer.getRegistry(); } - public GrpcSecurity userDetailsService(UserDetailsService userDetailsService) - throws Exception { + public GrpcSecurity userDetailsService(UserDetailsService userDetailsService) { getAuthenticationRegistry().userDetailsService(userDetailsService); return this; } + @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; @@ -77,53 +65,43 @@ public GrpcSecurity authenticationProvider(AuthenticationProvider authentication } @Override - protected void beforeConfigure() throws Exception { - + protected void beforeConfigure() { } @Override - protected ServerInterceptor performBuild() throws Exception { + protected ServerInterceptor performBuild() { + final GrpcSecurityMetadataSource metadataSource = getSharedObject(GrpcSecurityMetadataSource.class); + // Build expression handler with application context so that Spring beans + // referenced in @PreAuthorize/@PostAuthorize expressions (e.g. @permissionService) are resolved. + DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); + expressionHandler.setApplicationContext(getApplicationContext()); - final GrpcSecurityMetadataSource metadataSource =getSharedObject(GrpcSecurityMetadataSource.class); - DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler = new DefaultMethodSecurityExpressionHandler(); - methodSecurityExpressionHandler.setApplicationContext(getApplicationContext()); - final DelegatingMethodSecurityMetadataSource compositeMDS = new DelegatingMethodSecurityMetadataSource(Arrays.asList( + // PreAuthorize manager — handles @PreAuthorize annotations + PreAuthorizeAuthorizationManager preAuthorizeManager = new PreAuthorizeAuthorizationManager(); + preAuthorizeManager.setExpressionHandler(expressionHandler); + + // PostAuthorize manager — handles @PostAuthorize annotations + PostAuthorizeAuthorizationManager postAuthorizeManager = new PostAuthorizeAuthorizationManager(); + postAuthorizeManager.setExpressionHandler(expressionHandler); + + // Note: @Secured annotations are handled by GrpcServiceAuthorizationConfigurer.processSecuredAnnotation() + // at startup time and added to the GrpcSecurityMetadataSource. They are NOT handled by a + // SecuredAuthorizationManager here, to preserve AffirmativeBased semantics (if any manager + // grants, access is granted regardless of @Secured denial). + + final SecurityInterceptor securityInterceptor = new SecurityInterceptor( metadataSource, - new PrePostAnnotationSecurityMetadataSource( - new ExpressionBasedAnnotationAttributeFactory( - methodSecurityExpressionHandler - ) - ) - )); - final SecurityInterceptor securityInterceptor = new SecurityInterceptor(compositeMDS,getAuthenticationSchemeService()); - securityInterceptor.setAfterInvocationManager(afterInvocationManager()); - securityInterceptor.setAuthenticationManager(getSharedObject(AuthenticationManagerBuilder.class).build()); - final RoleVoter scopeVoter = new RoleVoter(); - scopeVoter.setRolePrefix("SCOPE_"); - - - - ExpressionBasedPreInvocationAdvice expressionAdvice = new ExpressionBasedPreInvocationAdvice(); - expressionAdvice.setExpressionHandler(methodSecurityExpressionHandler); - - - final AffirmativeBased accessDecisionManager = new AffirmativeBased(Arrays.asList( - new RoleVoter(), - scopeVoter, - new AuthenticatedAttributeVoter(), - new PreInvocationAuthorizationAdviceVoter(expressionAdvice){ - @Override - public int vote(Authentication authentication, MethodInvocation method, Collection attributes) { - // first time invoked without arguments - return null==method.getArguments() ? ACCESS_ABSTAIN: super.vote(authentication, method, attributes); - } - } - )); - accessDecisionManager.setAllowIfAllAbstainDecisions(true); - - securityInterceptor.setAccessDecisionManager(accessDecisionManager); + getAuthenticationSchemeService(), + preAuthorizeManager, + postAuthorizeManager + ); + + securityInterceptor.setAuthenticationManager( + getSharedObject(AuthenticationManagerBuilder.class).build() + ); + final GRpcServerProperties.SecurityProperties.Auth authCfg = Optional.of(applicationContext.getBean(GRpcServerProperties.class)) .map(GRpcServerProperties::getSecurity) .map(GRpcServerProperties.SecurityProperties::getAuth) @@ -131,37 +109,12 @@ public int vote(Authentication authentication, MethodInvocation method, Collecti securityInterceptor.setConfig(authCfg); return securityInterceptor; } - @SuppressWarnings("unchecked") - private > C getOrApply(C configurer) throws Exception { - C existingConfig = (C) getConfigurer(configurer.getClass()); - if (existingConfig != null) { - return existingConfig; - } - return apply(configurer); - } + private AuthenticationManagerBuilder getAuthenticationRegistry() { return getSharedObject(AuthenticationManagerBuilder.class); } + private AuthenticationSchemeService getAuthenticationSchemeService() { return getSharedObject(AuthenticationSchemeService.class); } - - protected AfterInvocationManager afterInvocationManager() { - - AfterInvocationProviderManager invocationProviderManager = new AfterInvocationProviderManager(); - ExpressionBasedPostInvocationAdvice postAdvice = new ExpressionBasedPostInvocationAdvice( - new DefaultMethodSecurityExpressionHandler()); - PostInvocationAdviceProvider postInvocationAdviceProvider = new PostInvocationAdviceProvider(postAdvice); - - - - invocationProviderManager.setProviders(Arrays.asList( - postInvocationAdviceProvider - )); - invocationProviderManager.afterPropertiesSet(); - return invocationProviderManager; - - } - - } diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurityConfiguration.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurityConfiguration.java index 5b2dde8d..f39d2f73 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurityConfiguration.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurityConfiguration.java @@ -19,11 +19,12 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration; +import org.springframework.boot.security.autoconfigure.UserDetailsServiceAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; import org.springframework.security.access.AccessDeniedException; -import org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor; +import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManagerResolver; import org.springframework.security.authentication.AuthenticationProvider; @@ -54,12 +55,13 @@ public static BeanPostProcessor bypassMethodInterceptorForGrpcMethodInvocation() return new BeanPostProcessor() { @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { - if(bean instanceof MethodSecurityInterceptor){ + if(bean instanceof AuthorizationManagerBeforeMethodInterceptor){ + final AuthorizationManagerBeforeMethodInterceptor interceptor = (AuthorizationManagerBeforeMethodInterceptor) bean; return (MethodInterceptor) invocation -> { if (BindableService.class.isAssignableFrom(invocation.getMethod().getDeclaringClass())){ return invocation.proceed(); } - return ((MethodSecurityInterceptor) bean).invoke(invocation); + return interceptor.invoke(invocation); }; } return bean; @@ -128,20 +130,13 @@ public BasicAuthSchemeSelector basicAuthSchemeSelector() { return new BasicAuthSchemeSelector(); } - @Bean - @ConditionalOnClass(name = { - "org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken", - "org.springframework.security.oauth2.core.OAuth2AuthenticationException"}) - public BearerTokenAuthSchemeSelector bearerTokenAuthSchemeSelector() { - return new BearerTokenAuthSchemeSelector(); - } - @Configuration - @ConditionalOnClass(AuthenticationManager.class) + @ConditionalOnClass(UserDetailsServiceAutoConfiguration.class) @ConditionalOnBean(ObjectPostProcessor.class) @ConditionalOnMissingBean(value = { AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class, AuthenticationManagerResolver.class }, type = "org.springframework.security.oauth2.jwt.JwtDecoder") - static class DefaultUserDetailsServiceAutoConfiguration extends UserDetailsServiceAutoConfiguration {} + @Import(UserDetailsServiceAutoConfiguration.class) + static class DefaultUserDetailsServiceAutoConfiguration {} @Autowired(required = false) @SuppressWarnings({ "rawtypes", "unchecked" }) diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurityConfigurerAdapter.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurityConfigurerAdapter.java index c17f7e12..ca33a8e3 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurityConfigurerAdapter.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurityConfigurerAdapter.java @@ -37,8 +37,9 @@ public void setApplicationContext(ApplicationContext context) throws Exception { } @Override - public void init(GrpcSecurity builder) throws Exception { - builder.apply(new GrpcServiceAuthorizationConfigurer(builder.getApplicationContext().getBean(GRpcServicesRegistry.class))); + public void init(GrpcSecurity builder) { + GrpcServiceAuthorizationConfigurer authConfigurer = new GrpcServiceAuthorizationConfigurer(builder.getApplicationContext().getBean(GRpcServicesRegistry.class)); + builder.with(authConfigurer); builder.setSharedObject(AuthenticationManagerBuilder.class, authenticationManagerBuilder); final AuthenticationSchemeService authenticationSchemeService = new AuthenticationSchemeService(); @@ -54,7 +55,7 @@ public void init(GrpcSecurity builder) throws Exception { @Override - public void configure(GrpcSecurity builder) throws Exception { + public void configure(GrpcSecurity builder) { try { final Class jwtDecoderClass = Class.forName("org.springframework.security.oauth2.jwt.JwtDecoder"); final String[] beanNames = context.getBeanNamesForType(jwtDecoderClass); diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurityMetadataSource.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurityMetadataSource.java index 90e4992e..235ced7c 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurityMetadataSource.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurityMetadataSource.java @@ -2,50 +2,58 @@ import io.grpc.MethodDescriptor; import org.lognet.springboot.grpc.GRpcServicesRegistry; -import org.springframework.security.access.ConfigAttribute; -import org.springframework.security.access.method.MethodSecurityMetadataSource; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.authorization.AuthorizationResult; import java.lang.reflect.Method; -import java.util.Collection; -import java.util.List; import java.util.Map; -import java.util.stream.Collectors; - -public class GrpcSecurityMetadataSource implements MethodSecurityMetadataSource { - private Map, List> methodDescriptorAttributes; - private GRpcServicesRegistry registry; - - - public GrpcSecurityMetadataSource(GRpcServicesRegistry registry , Map, List> methodDescriptorAttributes) { - this.methodDescriptorAttributes = methodDescriptorAttributes; +import java.util.function.Supplier; + +/** + * Holds a mapping from gRPC {@link MethodDescriptor} to + * {@link AuthorizationManager} so the {@link SecurityInterceptor} can look up + * the gRPC-specific (metadata-driven) authorization rule for each incoming call. + * + *

The authorization manager stored here covers only the rules that were + * registered programmatically via + * {@link GrpcServiceAuthorizationConfigurer.Registry} (e.g. {@code anyMethod().authenticated()}). + * Annotation-based rules ({@code @PreAuthorize}, {@code @Secured}, etc.) are + * applied by separate managers in {@link SecurityInterceptor} and are NOT stored here. + */ +public class GrpcSecurityMetadataSource { + + private final Map, AuthorizationManager>> methodDescriptorManagers; + private final GRpcServicesRegistry registry; + + public GrpcSecurityMetadataSource( + GRpcServicesRegistry registry, + Map, AuthorizationManager>> methodDescriptorManagers) { + this.methodDescriptorManagers = methodDescriptorManagers; this.registry = registry; - - - } - - @Override - public Collection getAttributes(Object object) throws IllegalArgumentException { - final MethodDescriptor methodDescriptor = SecurityInterceptor.GrpcMethodInvocation.class.cast(object).getCall().getMethodDescriptor(); - return methodDescriptorAttributes.get(methodDescriptor); - } - - @Override - public Collection getAllConfigAttributes() { - return methodDescriptorAttributes - .values() - .stream() - .flatMap(Collection::stream) - .collect(Collectors.toList()); } - @Override - public boolean supports(Class clazz) { - return SecurityInterceptor.GrpcMethodInvocation.class.isAssignableFrom(clazz); + /** + * Returns the {@link AuthorizationManager} registered for the gRPC method + * represented by the given {@link SecurityInterceptor.GrpcMethodInvocation}, or + * {@code null} if no rule was registered for that method. + */ + @SuppressWarnings("unchecked") + public AuthorizationManager> getAuthorizationManager( + SecurityInterceptor.GrpcMethodInvocation invocation) { + MethodDescriptor descriptor = invocation.getCall().getMethodDescriptor(); + return methodDescriptorManagers.get(descriptor); } - @Override - public Collection getAttributes(Method method, Class targetClass) { - final MethodDescriptor methodDescriptor = registry.getMethodDescriptor(method); - return methodDescriptorAttributes.get(methodDescriptor); + /** + * Returns the {@link AuthorizationManager} registered for the Java + * {@link Method}, or {@code null} if no rule was registered for that method. + * Used to support annotation scanning path. + */ + public AuthorizationManager> getAuthorizationManager(Method method) { + MethodDescriptor descriptor = registry.getMethodDescriptor(method); + if (descriptor == null) { + return null; + } + return methodDescriptorManagers.get(descriptor); } } diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcServiceAuthorizationConfigurer.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcServiceAuthorizationConfigurer.java index 8523f5aa..8bc092ed 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcServiceAuthorizationConfigurer.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcServiceAuthorizationConfigurer.java @@ -4,15 +4,18 @@ import org.lognet.springboot.grpc.GRpcServicesRegistry; import org.springframework.beans.factory.BeanCreationException; import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.security.access.ConfigAttribute; -import org.springframework.security.access.SecurityConfig; import org.springframework.security.access.annotation.Secured; +import org.springframework.security.authorization.AuthenticatedAuthorizationManager; +import org.springframework.security.authorization.AuthorityAuthorizationManager; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.authorization.AuthorizationManagers; +import org.springframework.security.authorization.AuthorizationResult; import org.springframework.security.config.annotation.SecurityConfigurerAdapter; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; import java.util.*; import java.util.function.Predicate; +import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -30,9 +33,10 @@ public Registry getRegistry() { } @Override - public void configure(GrpcSecurity builder) throws Exception { + public void configure(GrpcSecurity builder) { registry.processSecuredAnnotation(); - builder.setSharedObject(GrpcSecurityMetadataSource.class, new GrpcSecurityMetadataSource(registry.servicesRegistry, registry.securedMethods)); + builder.setSharedObject(GrpcSecurityMetadataSource.class, + new GrpcSecurityMetadataSource(registry.servicesRegistry, registry.methodManagers)); } @@ -50,7 +54,7 @@ private AuthorizedMethod(ServiceDescriptor... serviceDescriptor) { } public GrpcServiceAuthorizationConfigurer.Registry authenticated() { - GrpcServiceAuthorizationConfigurer.this.registry.map(methods); + GrpcServiceAuthorizationConfigurer.this.registry.mapAuthenticated(methods); return GrpcServiceAuthorizationConfigurer.this.registry; } @@ -67,9 +71,7 @@ public GrpcServiceAuthorizationConfigurer.Registry hasAnyRole(String... roles) { } public GrpcServiceAuthorizationConfigurer.Registry hasAnyAuthority(String... authorities) { - for (String auth : authorities) { - GrpcServiceAuthorizationConfigurer.this.registry.map(auth, methods); - } + GrpcServiceAuthorizationConfigurer.this.registry.mapAuthorities(authorities, methods); return GrpcServiceAuthorizationConfigurer.this.registry; } @@ -78,7 +80,21 @@ public GrpcServiceAuthorizationConfigurer.Registry hasAnyAuthority(String... aut public class Registry { - private MultiValueMap, ConfigAttribute> securedMethods = new LinkedMultiValueMap<>(); + /** + * Map from gRPC MethodDescriptor to the combined AuthorizationManager for that method. + * Multiple rules for the same method are combined with anyOf (same semantics as the + * old AffirmativeBased: grant if any voter grants). + */ + private Map, List>>> methodManagerLists + = new LinkedHashMap<>(); + + /** + * Flattened/combined view built lazily in {@code configure()} via + * {@link #buildMethodManagers()}. + */ + Map, AuthorizationManager>> methodManagers + = new LinkedHashMap<>(); + GRpcServicesRegistry servicesRegistry; private boolean withSecuredAnnotation = true; @@ -200,6 +216,7 @@ private void processSecuredAnnotation() { } } + buildMethodManagers(); } public AuthorizedMethod methods(MethodDescriptor... methodDescriptor) { @@ -210,18 +227,35 @@ public AuthorizedMethod services(ServiceDescriptor... serviceDescriptor) { return new AuthorizedMethod(serviceDescriptor); } - void map(List> methods) { - methods.forEach(m -> securedMethods.addAll(m, Collections.singletonList(new AuthenticatedConfigAttribute()))); - + void mapAuthenticated(List> methods) { + AuthorizationManager> manager = + AuthenticatedAuthorizationManager.authenticated(); + methods.forEach(m -> methodManagerLists.computeIfAbsent(m, k -> new ArrayList<>()).add(manager)); } - void map(String attribute, List> methods) { - methods.forEach(m -> securedMethods.addAll(m, SecurityConfig.createList(attribute))); + void mapAuthorities(String[] authorities, List> methods) { + AuthorizationManager> manager = + AuthorityAuthorizationManager.hasAnyAuthority(authorities); + methods.forEach(m -> methodManagerLists.computeIfAbsent(m, k -> new ArrayList<>()).add(manager)); + } + @SuppressWarnings("unchecked") + private void buildMethodManagers() { + for (Map.Entry, List>>> entry + : methodManagerLists.entrySet()) { + List>> managers = entry.getValue(); + AuthorizationManager> combined; + if (managers.size() == 1) { + combined = managers.get(0); + } else { + combined = AuthorizationManagers.anyOf(managers.toArray(new AuthorizationManager[0])); + } + methodManagers.put(entry.getKey(), combined); + } } public GrpcSecurity and() { - return GrpcServiceAuthorizationConfigurer.this.and(); + return GrpcServiceAuthorizationConfigurer.this.getBuilder(); } } } diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/SecurityInterceptor.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/SecurityInterceptor.java index 955305d7..ab079136 100644 --- a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/SecurityInterceptor.java +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/SecurityInterceptor.java @@ -13,9 +13,18 @@ import org.springframework.context.annotation.Lazy; import org.springframework.core.Ordered; import org.springframework.security.access.AccessDeniedException; -import org.springframework.security.access.SecurityMetadataSource; -import org.springframework.security.access.intercept.AbstractSecurityInterceptor; -import org.springframework.security.access.intercept.InterceptorStatusToken; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationTrustResolver; +import org.springframework.security.authentication.AuthenticationTrustResolverImpl; +import org.springframework.security.authentication.InsufficientAuthenticationException; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationDeniedException; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.authorization.AuthorizationManagers; +import org.springframework.security.authorization.AuthorizationResult; +import org.springframework.security.authorization.method.MethodInvocationResult; +import org.springframework.security.authorization.method.PostAuthorizeAuthorizationManager; +import org.springframework.security.authorization.method.PreAuthorizeAuthorizationManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContext; @@ -25,23 +34,38 @@ import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.Optional; +import java.util.function.Supplier; @Slf4j -public class SecurityInterceptor extends AbstractSecurityInterceptor implements ServerInterceptor, Ordered { +public class SecurityInterceptor implements ServerInterceptor, Ordered { - private static final Context.Key INTERCEPTOR_STATUS_TOKEN = Context.key("INTERCEPTOR_STATUS_TOKEN"); private static final Context.Key> METHOD_INVOCATION = Context.key("METHOD_INVOCATION"); - private final SecurityMetadataSource securityMetadataSource; + private final GrpcSecurityMetadataSource securityMetadataSource; private final AuthenticationSchemeSelector schemeSelector; + private AuthenticationManager authenticationManager; + private GRpcServerProperties.SecurityProperties.Auth authCfg; private FailureHandlingSupport failureHandlingSupport; private GRpcServicesRegistry registry; + /** Pre-authorization manager for {@code @PreAuthorize} annotations. */ + private PreAuthorizeAuthorizationManager preAuthorizeManager; + + /** Post-authorization manager for {@code @PostAuthorize} annotations. */ + private PostAuthorizeAuthorizationManager postAuthorizeManager; + + /** + * Used to determine whether a denied authorization result is due to + * the user being unauthenticated (UNAUTHENTICATED) rather than lacking + * a required authority (PERMISSION_DENIED). + */ + private final AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); + static class GrpcMethodInvocation extends SimpleMethodInvocation { final private ServerCall call; @@ -69,16 +93,20 @@ ServerCall getCall() { } - public SecurityInterceptor(SecurityMetadataSource securityMetadataSource, AuthenticationSchemeSelector schemeSelector) { + public SecurityInterceptor(GrpcSecurityMetadataSource securityMetadataSource, + AuthenticationSchemeSelector schemeSelector, + PreAuthorizeAuthorizationManager preAuthorizeManager, + PostAuthorizeAuthorizationManager postAuthorizeManager) { this.securityMetadataSource = securityMetadataSource; this.schemeSelector = schemeSelector; + this.preAuthorizeManager = preAuthorizeManager; + this.postAuthorizeManager = postAuthorizeManager; } @Autowired public void setGRpcServicesRegistry(GRpcServicesRegistry registry) { this.registry = registry; - } @Autowired @@ -90,42 +118,21 @@ public void setConfig(GRpcServerProperties.SecurityProperties.Auth authCfg) { this.authCfg = Optional.ofNullable(authCfg).orElseGet(GRpcServerProperties.SecurityProperties.Auth::new); } - @Override - public int getOrder() { - return Optional.ofNullable(authCfg.getInterceptorOrder()).orElse(Ordered.HIGHEST_PRECEDENCE + 1); - } - - @Override - public Class getSecureObjectClass() { - return GrpcMethodInvocation.class; + public void setAuthenticationManager(AuthenticationManager authenticationManager) { + this.authenticationManager = authenticationManager; } @Override - public SecurityMetadataSource obtainSecurityMetadataSource() { - return securityMetadataSource; + public int getOrder() { + return Optional.ofNullable(authCfg.getInterceptorOrder()).orElse(Ordered.HIGHEST_PRECEDENCE + 1); } @Override - /** - * Execute the same interceptor flow as original FilterSecurityInterceptor/MethodSecurityInterceptor - * { - * InterceptorStatusToken token = super.beforeInvocation(mi); - * Object result; - * try { - * result = mi.proceed(); - * } - * finally { - * super.finallyInvocation(token); - * } - * return super.afterInvocation(token, result); - * } - */ public ServerCall.Listener interceptCall( ServerCall call, Metadata headers, ServerCallHandler next) { - final CharSequence authorization = Optional.ofNullable(headers.get(Metadata.Key.of("Authorization" + Metadata.BINARY_HEADER_SUFFIX, Metadata.BINARY_BYTE_MARSHALLER))) .map(auth -> (CharSequence) StandardCharsets.UTF_8.decode(ByteBuffer.wrap(auth))) .orElse(headers.get(Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER))); @@ -147,7 +154,7 @@ public ServerCall.Listener interceptCall( private ServerCallHandler authenticationPropagatingHandler(ServerCallHandler next) { - return (call, headers) -> new ForwardingServerCallListener.SimpleForwardingServerCallListener(next.startCall(afterInvocationPropagator(call), headers)) { + return (call, headers) -> new ForwardingServerCallListener.SimpleForwardingServerCallListener(next.startCall(postAuthorizeInterceptingCall(call), headers)) { @Override public void onMessage(ReqT message) { @@ -168,10 +175,10 @@ public void onMessage(ReqT message) { break; default: log.error("Unsupported call type " + call.getMethodDescriptor().getType()); - throw new StatusRuntimeException(Status.UNAUTHENTICATED) ; + throw new StatusRuntimeException(Status.UNAUTHENTICATED); } - beforeInvocation(METHOD_INVOCATION.get()); + performPreAuthorization(METHOD_INVOCATION.get()); super.onMessage(message); } catch (RuntimeException e) { failureHandlingSupport.closeCall(e, call, headers); @@ -180,19 +187,13 @@ public void onMessage(ReqT message) { } finally { METHOD_INVOCATION.get().setArguments(null); } - - } ); } @Override public void onHalfClose() { - try { - propagateAuthentication(super::onHalfClose); - } finally { - finallyInvocation(INTERCEPTOR_STATUS_TOKEN.get()); - } + propagateAuthentication(super::onHalfClose); } @Override @@ -222,36 +223,172 @@ private void propagateAuthentication(Runnable runnable) { }; } - private ServerCall afterInvocationPropagator(ServerCall call) { + /** + * Wraps the call so that each outbound message is post-authorized before being sent. + */ + private ServerCall postAuthorizeInterceptingCall(ServerCall call) { return new ForwardingServerCall.SimpleForwardingServerCall(call) { @Override public void sendMessage(ReqT message) { - super.sendMessage((ReqT) afterInvocation(INTERCEPTOR_STATUS_TOKEN.get(), message)); + GrpcMethodInvocation mi = METHOD_INVOCATION.get(); + if (mi != null) { + MethodInvocationResult result = new MethodInvocationResult(mi, message); + Supplier authSupplier = () -> GrpcSecurity.AUTHENTICATION_CONTEXT_KEY.get(); + checkPostAuthorize(authSupplier, result); + } + super.sendMessage(message); } }; } + /** + * Builds the pre-authorization manager for the given method invocation. + * + *

Uses {@code anyOf} semantics (matching the old {@code AffirmativeBased} with + * {@code allowIfAllAbstainDecisions=true}): + *

    + *
  • Grant immediately if the gRPC-registry rule grants.
  • + *
  • Grant immediately if {@code @PreAuthorize} expression grants.
  • + *
  • Grant by default if ALL managers abstain (no rules apply).
  • + *
  • Deny only if at least one manager denies and none grants.
  • + *
+ * + *

Note: {@code @Secured} annotations are handled by + * {@link GrpcServiceAuthorizationConfigurer#processSecuredAnnotation()} at startup time + * and added to the gRPC registry metadata manager — they are NOT checked separately here. + * + * @param mi the method invocation (arguments may be null at call-setup time) + * @return the composite {@link AuthorizationManager} + */ + @SuppressWarnings("unchecked") + private AuthorizationManager> buildPreAuthManager(GrpcMethodInvocation mi) { + // gRPC-registry-driven rule for this specific method (may be null if no rule is registered) + AuthorizationManager> metaManager = + securityMetadataSource.getAuthorizationManager(mi); + + // @PreAuthorize wrapper: abstains when arguments are null (to mirror old behaviour of + // PreInvocationAuthorizationAdviceVoter that returned ACCESS_ABSTAIN when arguments==null) + AuthorizationManager> preAuthWrapper = (authSupplier, inv) -> { + if (inv.getArguments() == null) { + // Arguments not yet available — abstain, to be re-evaluated in onMessage() + return null; + } + try { + return preAuthorizeManager.authorize(authSupplier, inv); + } catch (AuthenticationException | AccessDeniedException e) { + throw e; + } catch (Exception e) { + // Expression evaluation failure (e.g. missing argument binding) — abstain + log.trace("@PreAuthorize expression evaluation failed, treating as abstain", e); + return null; + } + }; + + // Combine with anyOf semantics: + // - Grant if any grants (AffirmativeBased behavior) + // - Default GRANT if all abstain (allowIfAllAbstainDecisions=true) + if (metaManager != null) { + return AuthorizationManagers.anyOf( + new AuthorizationDecision(true), + metaManager, + preAuthWrapper + ); + } else { + // No registry rule for this method; only @PreAuthorize matters. + // Still use anyOf with default-grant so that methods with no rules at all are allowed. + return AuthorizationManagers.anyOf( + new AuthorizationDecision(true), + preAuthWrapper + ); + } + } + + /** + * Performs pre-authorization for the gRPC method invocation. + * Called from {@code onMessage()} with actual arguments set. + */ + private void performPreAuthorization(GrpcMethodInvocation mi) { + Supplier authSupplier = () -> SecurityContextHolder.getContext().getAuthentication(); + AuthorizationManager> manager = buildPreAuthManager(mi); + AuthorizationResult result = manager.authorize(authSupplier, mi); + if (result != null && !result.isGranted()) { + throwAccessDenied(authSupplier.get(), result); + } + } + + /** + * Performs post-authorization. Covers {@code @PostAuthorize} annotations. + */ + private void checkPostAuthorize(Supplier authSupplier, MethodInvocationResult result) { + AuthorizationResult postResult = postAuthorizeManager.authorize(authSupplier, result); + if (postResult != null && !postResult.isGranted()) { + throwAccessDenied(authSupplier.get(), postResult); + } + } + + /** + * Translates a denied authorization result into the appropriate exception: + *

    + *
  • If the user is not authenticated (null or anonymous), throws + * {@link InsufficientAuthenticationException} → gRPC UNAUTHENTICATED status.
  • + *
  • Otherwise, throws {@link AuthorizationDeniedException} → gRPC PERMISSION_DENIED status.
  • + *
+ * This mirrors the behaviour of Spring Security's {@code ExceptionTranslationFilter}. + */ + private void throwAccessDenied(Authentication authentication, AuthorizationResult result) { + if (authentication == null || trustResolver.isAnonymous(authentication)) { + throw new InsufficientAuthenticationException("Full authentication is required to access this resource"); + } + throw new AuthorizationDeniedException("Access Denied", result); + } + private Context setupGRpcSecurityContext(ServerCall call, Metadata headers, ServerCallHandler next, CharSequence authorization) { final Authentication authentication = null == authorization ? null : schemeSelector.getAuthScheme(authorization) .orElseThrow(() -> new StatusRuntimeException(Status.UNAUTHENTICATED)); + // If we have a pre-authentication token, run it through the AuthenticationManager to get + // a fully authenticated token (with granted authorities). + final Authentication authenticatedAuth; + if (authentication != null && authenticationManager != null && !authentication.isAuthenticated()) { + authenticatedAuth = authenticationManager.authenticate(authentication); + } else { + authenticatedAuth = authentication; + } + SecurityContext context = SecurityContextHolder.createEmptyContext(); - context.setAuthentication(authentication); + context.setAuthentication(authenticatedAuth); SecurityContextHolder.setContext(context); final GRpcServicesRegistry.GrpcServiceMethod grpcServiceMethod = registry.getGrpServiceMethod(call.getMethodDescriptor()); - final GrpcMethodInvocation methodInvocation = new GrpcMethodInvocation<>(grpcServiceMethod, call, headers, next); - final InterceptorStatusToken interceptorStatusToken = beforeInvocation(methodInvocation); + + // Perform the initial authorization check at call setup time. + // At this point, arguments are null, so @PreAuthorize with argument-dependent expressions + // will abstain (same behaviour as the old PreInvocationAuthorizationAdviceVoter). + // The full @PreAuthorize check (with arguments) happens in onMessage(). + performInitialAuthorizationCheck(methodInvocation); return Context.current() .withValue(GrpcSecurity.AUTHENTICATION_CONTEXT_KEY, SecurityContextHolder.getContext().getAuthentication()) - .withValue(INTERCEPTOR_STATUS_TOKEN, interceptorStatusToken) .withValue(METHOD_INVOCATION, methodInvocation); } + /** + * Performs the initial authorization check at call setup time (before any message has arrived). + * Uses the same composite manager as {@link #performPreAuthorization}, but at this point the + * method arguments are null so argument-dependent {@code @PreAuthorize} expressions will abstain. + */ + private void performInitialAuthorizationCheck(GrpcMethodInvocation mi) { + Supplier authSupplier = () -> SecurityContextHolder.getContext().getAuthentication(); + AuthorizationManager> manager = buildPreAuthManager(mi); + AuthorizationResult result = manager.authorize(authSupplier, mi); + if (result != null && !result.isGranted()) { + throwAccessDenied(authSupplier.get(), result); + } + } + private ServerCall.Listener fail(ServerCallHandler next, ServerCall call, Metadata headers, RuntimeException exception) throws RuntimeException { if (authCfg.isFailFast()) { diff --git a/grpc-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/grpc-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 85593974..54d3edce 100644 --- a/grpc-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/grpc-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -2,4 +2,5 @@ org.lognet.springboot.grpc.autoconfigure.GRpcAutoConfiguration org.lognet.springboot.grpc.autoconfigure.metrics.GRpcMetricsAutoConfiguration org.lognet.springboot.grpc.autoconfigure.consul.ConsulGrpcAutoConfiguration org.lognet.springboot.grpc.autoconfigure.security.SecurityAutoConfiguration +org.lognet.springboot.grpc.autoconfigure.security.BearerTokenSecurityAutoConfiguration org.lognet.springboot.grpc.autoconfigure.actuate.GRpcActuateAutoConfiguration