From 6dbcd0e95e6e88e02326a5e4a1a9df3a3bda0bb1 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 21 Dec 2021 08:33:50 +0100 Subject: [PATCH] Auto-configure security for Spring GraphQL This commit configures security features for Spring GraphQL. In the case of both MVC and WebFlux, this contributes `DataFetcherExceptionResolver` instances to resolve security exceptions and expose them as proper errors in the GraphQL response. For MVC only, this also configures a `SecurityContextThreadLocalAccessor`. This component ensures that the security context is propagated between `ThreadLocal` and the Reactor asynchronous execution. See gh-29140 --- ...aphQlWebFluxSecurityAutoConfiguration.java | 54 ++++++ ...raphQlWebMvcSecurityAutoConfiguration.java | 61 ++++++ .../graphql/security/package-info.java | 20 ++ .../main/resources/META-INF/spring.factories | 2 + ...WebFluxSecurityAutoConfigurationTests.java | 163 ++++++++++++++++ ...lWebMvcSecurityAutoConfigurationTests.java | 179 ++++++++++++++++++ 6 files changed, 479 insertions(+) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/security/GraphQlWebFluxSecurityAutoConfiguration.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/security/GraphQlWebMvcSecurityAutoConfiguration.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/security/package-info.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/security/GraphQlWebFluxSecurityAutoConfigurationTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/security/GraphQlWebMvcSecurityAutoConfigurationTests.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/security/GraphQlWebFluxSecurityAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/security/GraphQlWebFluxSecurityAutoConfiguration.java new file mode 100644 index 0000000000..6773857e70 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/security/GraphQlWebFluxSecurityAutoConfiguration.java @@ -0,0 +1,54 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.graphql.security; + +import graphql.GraphQL; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +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.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.graphql.reactive.GraphQlWebFluxAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.graphql.security.ReactiveSecurityDataFetcherExceptionResolver; +import org.springframework.graphql.web.webflux.GraphQlHttpHandler; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for enabling Security support for + * Spring GraphQL with WebFlux. + * + * @author Brian Clozel + * @since 2.7.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) +@ConditionalOnClass({ GraphQL.class, GraphQlHttpHandler.class, EnableWebFluxSecurity.class }) +@ConditionalOnBean(GraphQlHttpHandler.class) +@AutoConfigureAfter(GraphQlWebFluxAutoConfiguration.class) +public class GraphQlWebFluxSecurityAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public ReactiveSecurityDataFetcherExceptionResolver reactiveSecurityDataFetcherExceptionResolver() { + return new ReactiveSecurityDataFetcherExceptionResolver(); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/security/GraphQlWebMvcSecurityAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/security/GraphQlWebMvcSecurityAutoConfiguration.java new file mode 100644 index 0000000000..bff7bfc7d8 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/security/GraphQlWebMvcSecurityAutoConfiguration.java @@ -0,0 +1,61 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.graphql.security; + +import graphql.GraphQL; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +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.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.graphql.servlet.GraphQlWebMvcAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.graphql.security.SecurityContextThreadLocalAccessor; +import org.springframework.graphql.security.SecurityDataFetcherExceptionResolver; +import org.springframework.graphql.web.webmvc.GraphQlHttpHandler; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for enabling Security support for + * Spring GraphQL with MVC. + * + * @author Brian Clozel + * @since 2.7.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) +@ConditionalOnClass({ GraphQL.class, GraphQlHttpHandler.class, EnableWebSecurity.class }) +@ConditionalOnBean(GraphQlHttpHandler.class) +@AutoConfigureAfter(GraphQlWebMvcAutoConfiguration.class) +public class GraphQlWebMvcSecurityAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public SecurityDataFetcherExceptionResolver securityDataFetcherExceptionResolver() { + return new SecurityDataFetcherExceptionResolver(); + } + + @Bean + @ConditionalOnMissingBean + public SecurityContextThreadLocalAccessor securityContextThreadLocalAccessor() { + return new SecurityContextThreadLocalAccessor(); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/security/package-info.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/security/package-info.java new file mode 100644 index 0000000000..6d1c5e30bf --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/security/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Auto-configuration classes for Security support in Spring GraphQL. + */ +package org.springframework.boot.autoconfigure.graphql.security; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index e4b771a4cb..aef3e3399f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -75,6 +75,8 @@ org.springframework.boot.autoconfigure.graphql.data.GraphQlQueryByExampleAutoCon org.springframework.boot.autoconfigure.graphql.data.GraphQlQuerydslAutoConfiguration,\ org.springframework.boot.autoconfigure.graphql.reactive.GraphQlWebFluxAutoConfiguration,\ org.springframework.boot.autoconfigure.graphql.servlet.GraphQlWebMvcAutoConfiguration,\ +org.springframework.boot.autoconfigure.graphql.security.GraphQlWebFluxSecurityAutoConfiguration,\ +org.springframework.boot.autoconfigure.graphql.security.GraphQlWebMvcSecurityAutoConfiguration,\ org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration,\ org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration,\ org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfiguration,\ diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/security/GraphQlWebFluxSecurityAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/security/GraphQlWebFluxSecurityAutoConfigurationTests.java new file mode 100644 index 0000000000..9b6f1db534 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/security/GraphQlWebFluxSecurityAutoConfigurationTests.java @@ -0,0 +1,163 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.graphql.security; + +import java.util.Collections; +import java.util.function.Consumer; + +import graphql.schema.idl.TypeRuntimeWiring; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.graphql.Book; +import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration; +import org.springframework.boot.autoconfigure.graphql.GraphQlTestDataFetchers; +import org.springframework.boot.autoconfigure.graphql.reactive.GraphQlWebFluxAutoConfiguration; +import org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration; +import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration; +import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration; +import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.graphql.execution.ErrorType; +import org.springframework.graphql.execution.RuntimeWiringConfigurer; +import org.springframework.graphql.security.ReactiveSecurityDataFetcherExceptionResolver; +import org.springframework.http.MediaType; +import org.springframework.lang.Nullable; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.core.userdetails.MapReactiveUserDetailsService; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.security.config.Customizer.withDefaults; + +/** + * Tests for {@link GraphQlWebFluxSecurityAutoConfiguration}. + * + * @author Brian Clozel + */ +class GraphQlWebFluxSecurityAutoConfigurationTests { + + private static final String BASE_URL = "https://spring.example.org/graphql"; + + private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(HttpHandlerAutoConfiguration.class, WebFluxAutoConfiguration.class, + CodecsAutoConfiguration.class, JacksonAutoConfiguration.class, GraphQlAutoConfiguration.class, + GraphQlWebFluxAutoConfiguration.class, GraphQlWebFluxSecurityAutoConfiguration.class, + ReactiveSecurityAutoConfiguration.class)) + .withUserConfiguration(DataFetchersConfiguration.class, SecurityConfig.class) + .withPropertyValues("spring.main.web-application-type=reactive"); + + @Test + void contributesExceptionResolver() { + this.contextRunner.run( + (context) -> assertThat(context).hasSingleBean(ReactiveSecurityDataFetcherExceptionResolver.class)); + } + + @Test + void anonymousUserShouldBeUnauthorized() { + testWithWebClient((client) -> { + String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author }}"; + client.post().uri("").bodyValue("{ \"query\": \"" + query + "\"}").exchange().expectStatus().isOk() + .expectBody().jsonPath("data.bookById.name").doesNotExist() + .jsonPath("errors[0].extensions.classification").isEqualTo(ErrorType.UNAUTHORIZED.toString()); + }); + } + + @Test + void authenticatedUserShouldGetData() { + testWithWebClient((client) -> { + String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author }}"; + client.post().uri("").headers((headers) -> headers.setBasicAuth("rob", "rob")) + .bodyValue("{ \"query\": \"" + query + "\"}").exchange().expectStatus().isOk().expectBody() + .jsonPath("data.bookById.name").isEqualTo("GraphQL for beginners") + .jsonPath("errors[0].extensions.classification").doesNotExist(); + }); + } + + private void testWithWebClient(Consumer consumer) { + this.contextRunner.run((context) -> { + WebTestClient client = WebTestClient.bindToApplicationContext(context).configureClient() + .defaultHeaders((headers) -> { + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + }).baseUrl(BASE_URL).build(); + consumer.accept(client); + }); + } + + @Configuration(proxyBeanMethods = false) + static class DataFetchersConfiguration { + + @Bean + RuntimeWiringConfigurer bookDataFetcher(BookService bookService) { + return (builder) -> builder.type(TypeRuntimeWiring.newTypeWiring("Query").dataFetcher("bookById", + (env) -> bookService.getBookdById(env.getArgument("id")))); + } + + @Bean + BookService bookService() { + return new BookService(); + } + + } + + static class BookService { + + @PreAuthorize("hasRole('USER')") + @Nullable + Mono getBookdById(String id) { + return Mono.justOrEmpty(GraphQlTestDataFetchers.getBookById(id)); + } + + } + + @Configuration(proxyBeanMethods = false) + @EnableWebFluxSecurity + @EnableReactiveMethodSecurity + static class SecurityConfig { + + @Bean + SecurityWebFilterChain springWebFilterChain(ServerHttpSecurity http) throws Exception { + return http.csrf((spec) -> spec.disable()) + // Demonstrate that method security works + // Best practice to use both for defense in depth + .authorizeExchange((requests) -> requests.anyExchange().permitAll()).httpBasic(withDefaults()) + .build(); + } + + @Bean + @SuppressWarnings("deprecation") + MapReactiveUserDetailsService userDetailsService() { + User.UserBuilder userBuilder = User.withDefaultPasswordEncoder(); + UserDetails rob = userBuilder.username("rob").password("rob").roles("USER").build(); + UserDetails admin = userBuilder.username("admin").password("admin").roles("USER", "ADMIN").build(); + return new MapReactiveUserDetailsService(rob, admin); + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/security/GraphQlWebMvcSecurityAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/security/GraphQlWebMvcSecurityAutoConfigurationTests.java new file mode 100644 index 0000000000..ebdf9a7d40 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/security/GraphQlWebMvcSecurityAutoConfigurationTests.java @@ -0,0 +1,179 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.graphql.security; + +import graphql.schema.idl.TypeRuntimeWiring; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.graphql.Book; +import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration; +import org.springframework.boot.autoconfigure.graphql.GraphQlTestDataFetchers; +import org.springframework.boot.autoconfigure.graphql.servlet.GraphQlWebMvcAutoConfiguration; +import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.graphql.execution.ErrorType; +import org.springframework.graphql.execution.RuntimeWiringConfigurer; +import org.springframework.graphql.security.SecurityContextThreadLocalAccessor; +import org.springframework.graphql.security.SecurityDataFetcherExceptionResolver; +import org.springframework.http.MediaType; +import org.springframework.lang.Nullable; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.DefaultSecurityFilterChain; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.security.config.Customizer.withDefaults; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Tests for {@link GraphQlWebMvcSecurityAutoConfiguration}. + * + * @author Brian Clozel + */ +class GraphQlWebMvcSecurityAutoConfigurationTests { + + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + .withConfiguration( + AutoConfigurations.of(DispatcherServletAutoConfiguration.class, WebMvcAutoConfiguration.class, + HttpMessageConvertersAutoConfiguration.class, JacksonAutoConfiguration.class, + GraphQlAutoConfiguration.class, GraphQlWebMvcAutoConfiguration.class, + GraphQlWebMvcSecurityAutoConfiguration.class, SecurityAutoConfiguration.class)) + .withUserConfiguration(DataFetchersConfiguration.class, SecurityConfig.class) + .withPropertyValues("spring.main.web-application-type=servlet"); + + @Test + void contributesSecurityComponents() { + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(SecurityDataFetcherExceptionResolver.class); + assertThat(context).hasSingleBean(SecurityContextThreadLocalAccessor.class); + }); + } + + @Test + void anonymousUserShouldBeUnauthorized() { + testWith((mockMvc) -> { + String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author }}"; + MvcResult result = mockMvc.perform(post("/graphql").content("{\"query\": \"" + query + "\"}")).andReturn(); + mockMvc.perform(asyncDispatch(result)).andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("data.bookById.name").doesNotExist()).andExpect( + jsonPath("errors[0].extensions.classification").value(ErrorType.UNAUTHORIZED.toString())); + }); + } + + @Test + void authenticatedUserShouldGetData() { + testWith((mockMvc) -> { + String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author }}"; + MvcResult result = mockMvc + .perform(post("/graphql").content("{\"query\": \"" + query + "\"}").with(user("rob"))).andReturn(); + mockMvc.perform(asyncDispatch(result)).andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("data.bookById.name").value("GraphQL for beginners")) + .andExpect(jsonPath("errors").doesNotExist()); + }); + + } + + private void testWith(MockMvcConsumer mockMvcConsumer) { + this.contextRunner.run((context) -> { + MediaType mediaType = MediaType.APPLICATION_JSON; + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(context) + .defaultRequest(post("/graphql").contentType(mediaType).accept(mediaType)).apply(springSecurity()) + .build(); + mockMvcConsumer.accept(mockMvc); + }); + } + + private interface MockMvcConsumer { + + void accept(MockMvc mockMvc) throws Exception; + + } + + @Configuration(proxyBeanMethods = false) + static class DataFetchersConfiguration { + + @Bean + RuntimeWiringConfigurer bookDataFetcher(BookService bookService) { + return (builder) -> builder.type(TypeRuntimeWiring.newTypeWiring("Query").dataFetcher("bookById", + (env) -> bookService.getBookdById(env.getArgument("id")))); + } + + @Bean + BookService bookService() { + return new BookService(); + } + + } + + static class BookService { + + @PreAuthorize("hasRole('USER')") + @Nullable + Book getBookdById(String id) { + return GraphQlTestDataFetchers.getBookById(id); + } + + } + + @Configuration(proxyBeanMethods = false) + @EnableWebSecurity + @EnableGlobalMethodSecurity(prePostEnabled = true) + static class SecurityConfig { + + @Bean + DefaultSecurityFilterChain springWebFilterChain(HttpSecurity http) throws Exception { + return http.csrf((c) -> c.disable()) + // Demonstrate that method security works + // Best practice to use both for defense in depth + .authorizeRequests((requests) -> requests.anyRequest().permitAll()).httpBasic(withDefaults()) + .build(); + } + + @Bean + static InMemoryUserDetailsManager userDetailsService() { + User.UserBuilder userBuilder = User.withDefaultPasswordEncoder(); + UserDetails rob = userBuilder.username("rob").password("rob").roles("USER").build(); + UserDetails admin = userBuilder.username("admin").password("admin").roles("USER", "ADMIN").build(); + return new InMemoryUserDetailsManager(rob, admin); + } + + } + +}