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
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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,\
|
||||
|
||||
@@ -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<WebTestClient> 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<Book> 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user