diff --git a/gradle.properties b/gradle.properties index 5c61e6b1..d96762b2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -44,5 +44,3 @@ PLUGIN_JACOCO_VER=0.8.7 PLUGIN_SONARQUBE_VER=3.2.0 PLUGIN_NEXUS_STAGING_VER=0.30.0 PLUGIN_GOOGLE_JAVA_FORMAT_VER=0.9 -### -org.gradle.daemon=true diff --git a/graphql-spring-boot-autoconfigure/build.gradle b/graphql-spring-boot-autoconfigure/build.gradle index 0fdf2462..ec14f63c 100644 --- a/graphql-spring-boot-autoconfigure/build.gradle +++ b/graphql-spring-boot-autoconfigure/build.gradle @@ -54,7 +54,7 @@ dependencies { testImplementation "org.springframework.boot:spring-boot-starter-web" testImplementation "org.springframework.boot:spring-boot-starter-actuator" testImplementation "org.springframework.boot:spring-boot-starter-webflux" -// testImplementation "org.springframework.boot:spring-boot-starter-security" + testImplementation "org.springframework.boot:spring-boot-starter-security" testImplementation "org.springframework.security:spring-security-test" testImplementation "io.projectreactor:reactor-core" testImplementation "io.reactivex.rxjava2:rxjava" diff --git a/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/autoconfigure/editor/EditorConstants.java b/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/autoconfigure/editor/EditorConstants.java new file mode 100644 index 00000000..6ccef776 --- /dev/null +++ b/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/autoconfigure/editor/EditorConstants.java @@ -0,0 +1,10 @@ +package graphql.kickstart.autoconfigure.editor; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class EditorConstants { + + public static final String CSRF_ATTRIBUTE_NAME = "_csrf"; +} diff --git a/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/autoconfigure/editor/altair/AltairAutoConfiguration.java b/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/autoconfigure/editor/altair/AltairAutoConfiguration.java index fe0c5b4c..d01f8acd 100644 --- a/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/autoconfigure/editor/altair/AltairAutoConfiguration.java +++ b/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/autoconfigure/editor/altair/AltairAutoConfiguration.java @@ -17,7 +17,10 @@ public class AltairAutoConfiguration { @Bean @ConditionalOnProperty(value = "graphql.altair.enabled", havingValue = "true") - AltairController altairController() { - return new AltairController(); + AltairController altairController( + AltairProperties altairProperties, + AltairOptions altairOptions, + AltairResources altairResources) { + return new AltairController(altairProperties, altairOptions, altairResources); } } diff --git a/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/autoconfigure/editor/altair/AltairController.java b/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/autoconfigure/editor/altair/AltairController.java index dfcbf92e..af73eea8 100644 --- a/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/autoconfigure/editor/altair/AltairController.java +++ b/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/autoconfigure/editor/altair/AltairController.java @@ -13,12 +13,12 @@ import java.util.Map; import javax.annotation.PostConstruct; import javax.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import lombok.val; import org.apache.commons.lang3.StringUtils; import org.apache.commons.text.StringSubstitutor; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.ClassPathResource; import org.springframework.stereotype.Controller; import org.springframework.util.StreamUtils; @@ -27,14 +27,15 @@ /** @author Moncef AOUDIA */ @Slf4j @Controller +@RequiredArgsConstructor public class AltairController { private static final String CDN_JSDELIVR_NET_NPM = "//cdn.jsdelivr.net/npm/"; private static final String ALTAIR = "altair-static"; private final ObjectMapper objectMapper = new ObjectMapper(); - @Autowired private AltairProperties altairProperties; - @Autowired private AltairOptions altairOptions; - @Autowired private AltairResources altairResources; + private final AltairProperties altairProperties; + private final AltairOptions altairOptions; + private final AltairResources altairResources; private String template; diff --git a/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/autoconfigure/editor/voyager/ReactiveVoyagerController.java b/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/autoconfigure/editor/voyager/ReactiveVoyagerController.java index 318ea7e6..a1fb8a36 100644 --- a/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/autoconfigure/editor/voyager/ReactiveVoyagerController.java +++ b/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/autoconfigure/editor/voyager/ReactiveVoyagerController.java @@ -1,5 +1,7 @@ package graphql.kickstart.autoconfigure.editor.voyager; +import static graphql.kickstart.autoconfigure.editor.EditorConstants.CSRF_ATTRIBUTE_NAME; + import java.io.IOException; import java.util.Map; import lombok.RequiredArgsConstructor; @@ -9,6 +11,7 @@ import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestAttribute; /** @author Max David Günther */ @Controller @@ -18,10 +21,12 @@ public class ReactiveVoyagerController { @Autowired private VoyagerIndexHtmlTemplate indexTemplate; @GetMapping(path = "${graphql.voyager.mapping:/voyager}") - public ResponseEntity voyager(@PathVariable Map params) + public ResponseEntity voyager( + final @RequestAttribute(value = CSRF_ATTRIBUTE_NAME, required = false) Object csrf, + @PathVariable Map params) throws IOException { // no context path in spring-webflux - String indexHtmlContent = indexTemplate.fillIndexTemplate("", params); + String indexHtmlContent = indexTemplate.fillIndexTemplate("", csrf, params); return ResponseEntity.ok() .contentType(MediaType.valueOf("text/html; charset=UTF-8")) .body(indexHtmlContent); diff --git a/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/autoconfigure/editor/voyager/VoyagerAutoConfiguration.java b/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/autoconfigure/editor/voyager/VoyagerAutoConfiguration.java index c6ed9574..20e8e6bd 100644 --- a/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/autoconfigure/editor/voyager/VoyagerAutoConfiguration.java +++ b/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/autoconfigure/editor/voyager/VoyagerAutoConfiguration.java @@ -17,8 +17,8 @@ public class VoyagerAutoConfiguration { @Bean - VoyagerController voyagerController() { - return new VoyagerController(); + VoyagerController voyagerController(VoyagerIndexHtmlTemplate voyagerIndexHtmlTemplate) { + return new VoyagerController(voyagerIndexHtmlTemplate); } @Bean diff --git a/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/autoconfigure/editor/voyager/VoyagerController.java b/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/autoconfigure/editor/voyager/VoyagerController.java index 8e1ebd70..bb6fde1f 100644 --- a/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/autoconfigure/editor/voyager/VoyagerController.java +++ b/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/autoconfigure/editor/voyager/VoyagerController.java @@ -1,26 +1,33 @@ package graphql.kickstart.autoconfigure.editor.voyager; +import static graphql.kickstart.autoconfigure.editor.EditorConstants.CSRF_ATTRIBUTE_NAME; + import java.io.IOException; import java.util.Map; import javax.servlet.http.HttpServletRequest; -import org.springframework.beans.factory.annotation.Autowired; +import lombok.RequiredArgsConstructor; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestAttribute; /** @author Max David Günther */ @Controller +@RequiredArgsConstructor public class VoyagerController { - @Autowired private VoyagerIndexHtmlTemplate indexTemplate; + private final VoyagerIndexHtmlTemplate indexTemplate; @GetMapping(value = "${graphql.voyager.mapping:/voyager}") public ResponseEntity voyager( - HttpServletRequest request, @PathVariable Map params) throws IOException { + HttpServletRequest request, + final @RequestAttribute(value = CSRF_ATTRIBUTE_NAME, required = false) Object csrf, + @PathVariable Map params) + throws IOException { String contextPath = request.getContextPath(); - String indexHtmlContent = indexTemplate.fillIndexTemplate(contextPath, params); + String indexHtmlContent = indexTemplate.fillIndexTemplate(contextPath, csrf, params); return ResponseEntity.ok() .contentType(MediaType.valueOf("text/html; charset=UTF-8")) .body(indexHtmlContent); diff --git a/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/autoconfigure/editor/voyager/VoyagerIndexHtmlTemplate.java b/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/autoconfigure/editor/voyager/VoyagerIndexHtmlTemplate.java index d8c598b1..c15f9115 100644 --- a/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/autoconfigure/editor/voyager/VoyagerIndexHtmlTemplate.java +++ b/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/autoconfigure/editor/voyager/VoyagerIndexHtmlTemplate.java @@ -1,5 +1,8 @@ package graphql.kickstart.autoconfigure.editor.voyager; +import static graphql.kickstart.autoconfigure.editor.EditorConstants.CSRF_ATTRIBUTE_NAME; + +import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; import java.nio.charset.Charset; import java.util.HashMap; @@ -21,9 +24,10 @@ public class VoyagerIndexHtmlTemplate { private static final String FAVICON_APIS_GURU = "//apis.guru/graphql-voyager/icons/favicon-16x16.png"; + private final ObjectMapper objectMapper = new ObjectMapper(); private final VoyagerPropertiesConfiguration voyagerConfiguration; - public String fillIndexTemplate(String contextPath, Map params) + public String fillIndexTemplate(String contextPath, Object csrf, Map params) throws IOException { String template = StreamUtils.copyToString( @@ -34,6 +38,11 @@ public String fillIndexTemplate(String contextPath, Map params) String voyagerCdnVersion = voyagerConfiguration.getCdn().getVersion(); Map replacements = new HashMap<>(); + if (csrf != null) { + replacements.put(CSRF_ATTRIBUTE_NAME, objectMapper.writeValueAsString(csrf)); + } else { + replacements.put(CSRF_ATTRIBUTE_NAME, "null"); + } replacements.put("graphqlEndpoint", constructGraphQlEndpoint(contextPath, params)); replacements.put("pageTitle", voyagerConfiguration.getPageTitle()); replacements.put("pageFavicon", getResourceUrl(basePath, "favicon.ico", FAVICON_APIS_GURU)); diff --git a/graphql-spring-boot-autoconfigure/src/main/resources/templates/voyager.html b/graphql-spring-boot-autoconfigure/src/main/resources/templates/voyager.html index bfbb57ae..332ed226 100644 --- a/graphql-spring-boot-autoconfigure/src/main/resources/templates/voyager.html +++ b/graphql-spring-boot-autoconfigure/src/main/resources/templates/voyager.html @@ -1,88 +1,105 @@ - - - - - + + + + + - - - - + + + + - - - ${pageTitle} - - - -
-
-
-
-
- Loading... ; -
-
🛰 Powered by GraphQL Voyager
-
+ + + ${pageTitle} + + + +
+
+
+
+
+ Loading... ;
-
-
+
🛰 Powered by GraphQL + Voyager
+
+
+
+
- +

Transmitting...

-
-
- - + // Render into the body. + GraphQLVoyager.init(document.getElementById('voyager'), { + introspection: introspectionProvider, + displayOptions: { + skipRelay: ${voyagerDisplayOptionsSkipRelay}, + skipDeprecated: ${voyagerDisplayOptionsSkipDeprecated}, + rootType: '${voyagerDisplayOptionsRootType}', + sortByAlphabet: ${voyagerDisplayOptionsSortByAlphabet}, + showLeafFields: ${voyagerDisplayOptionsShowLeafFields}, + hideRoot: ${voyagerDisplayOptionsHideRoot}, + }, + hideDocs: ${voyagerHideDocs}, + hideSettings: ${voyagerHideSettings}, + }) + + diff --git a/graphql-spring-boot-autoconfigure/src/test/java/graphql/kickstart/autoconfigure/PermitAllWebSecurity.java b/graphql-spring-boot-autoconfigure/src/test/java/graphql/kickstart/autoconfigure/PermitAllWebSecurity.java new file mode 100644 index 00000000..fd713e57 --- /dev/null +++ b/graphql-spring-boot-autoconfigure/src/test/java/graphql/kickstart/autoconfigure/PermitAllWebSecurity.java @@ -0,0 +1,16 @@ +package graphql.kickstart.autoconfigure; + +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; + +@Configuration +@EnableWebSecurity +public class PermitAllWebSecurity extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(final HttpSecurity http) throws Exception { + http.authorizeRequests().anyRequest().permitAll(); + } +} diff --git a/graphql-spring-boot-autoconfigure/src/test/java/graphql/kickstart/autoconfigure/editor/voyager/VoyagerWithCsrfTest.java b/graphql-spring-boot-autoconfigure/src/test/java/graphql/kickstart/autoconfigure/editor/voyager/VoyagerWithCsrfTest.java new file mode 100644 index 00000000..80759313 --- /dev/null +++ b/graphql-spring-boot-autoconfigure/src/test/java/graphql/kickstart/autoconfigure/editor/voyager/VoyagerWithCsrfTest.java @@ -0,0 +1,46 @@ +package graphql.kickstart.autoconfigure.editor.voyager; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import graphql.kickstart.autoconfigure.PermitAllWebSecurity; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; + +@ExtendWith(SpringExtension.class) +@Import(PermitAllWebSecurity.class) +@SpringBootTest( + classes = {VoyagerAutoConfiguration.class, SecurityAutoConfiguration.class}, + properties = {"spring.main.web-application-type=servlet"}) +@AutoConfigureMockMvc +@ActiveProfiles("voyager") +class VoyagerWithCsrfTest { + + @Autowired private MockMvc mockMvc; + + @Test + void shouldLoadCSRFData() throws Exception { + final MvcResult mvcResult = + mockMvc.perform(get("/voyager")).andExpect(status().isOk()).andReturn(); + + final Document document = Jsoup.parse(mvcResult.getResponse().getContentAsString()); + final String script = document.body().select("body script").dataNodes().get(0).getWholeData(); + assertThat(script) + .contains("let csrf = {\"") + .contains("\"token\":\"") + .contains("\"parameterName\":\"_csrf\"") + .contains("\"headerName\":\"X-CSRF-TOKEN\""); + } +} diff --git a/graphql-spring-boot-autoconfigure/src/test/java/graphql/kickstart/autoconfigure/editor/voyager/VoyagerWithoutCsrfTest.java b/graphql-spring-boot-autoconfigure/src/test/java/graphql/kickstart/autoconfigure/editor/voyager/VoyagerWithoutCsrfTest.java new file mode 100644 index 00000000..7507cc12 --- /dev/null +++ b/graphql-spring-boot-autoconfigure/src/test/java/graphql/kickstart/autoconfigure/editor/voyager/VoyagerWithoutCsrfTest.java @@ -0,0 +1,36 @@ +package graphql.kickstart.autoconfigure.editor.voyager; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; + +@ExtendWith(SpringExtension.class) +@SpringBootTest(classes = {VoyagerAutoConfiguration.class}) +@AutoConfigureMockMvc +@ActiveProfiles("voyager") +class VoyagerWithoutCsrfTest { + + @Autowired private MockMvc mockMvc; + + @Test + void shouldLoadCSRFData() throws Exception { + final MvcResult mvcResult = + mockMvc.perform(get("/voyager")).andExpect(status().isOk()).andReturn(); + + final Document document = Jsoup.parse(mvcResult.getResponse().getContentAsString()); + final String script = document.body().select("body script").dataNodes().get(0).getWholeData(); + assertThat(script).contains("let csrf = null"); + } +} diff --git a/graphql-spring-boot-autoconfigure/src/test/java/graphql/kickstart/autoconfigure/web/reactive/MonoAutoConfigurationTest.java b/graphql-spring-boot-autoconfigure/src/test/java/graphql/kickstart/autoconfigure/web/reactive/MonoAutoConfigurationTest.java index 10816035..4fccbeb4 100644 --- a/graphql-spring-boot-autoconfigure/src/test/java/graphql/kickstart/autoconfigure/web/reactive/MonoAutoConfigurationTest.java +++ b/graphql-spring-boot-autoconfigure/src/test/java/graphql/kickstart/autoconfigure/web/reactive/MonoAutoConfigurationTest.java @@ -9,6 +9,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.context.junit.jupiter.SpringExtension; diff --git a/graphql-spring-boot-autoconfigure/src/test/java/graphql/kickstart/autoconfigure/web/reactive/MonoGenericWrapperAlreadyDefinedTest.java b/graphql-spring-boot-autoconfigure/src/test/java/graphql/kickstart/autoconfigure/web/reactive/MonoGenericWrapperAlreadyDefinedTest.java index 79839de3..8562ee45 100644 --- a/graphql-spring-boot-autoconfigure/src/test/java/graphql/kickstart/autoconfigure/web/reactive/MonoGenericWrapperAlreadyDefinedTest.java +++ b/graphql-spring-boot-autoconfigure/src/test/java/graphql/kickstart/autoconfigure/web/reactive/MonoGenericWrapperAlreadyDefinedTest.java @@ -4,6 +4,7 @@ import graphql.kickstart.tools.SchemaParserOptions.GenericWrapper; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import lombok.val; import org.json.JSONException; import org.json.JSONObject; @@ -18,6 +19,7 @@ import org.springframework.test.web.reactive.server.WebTestClient; import reactor.core.publisher.Mono; +@Slf4j @RequiredArgsConstructor @ExtendWith(SpringExtension.class) @SpringBootTest( @@ -41,6 +43,7 @@ void monoWrapper() throws JSONException { .exchange() .returnResult(String.class); val response = result.getResponseBody().blockFirst(); + log.info("Response: {}", response); val json = new JSONObject(response); assertThat(json.getJSONObject("data").get("hello")).isEqualTo("Hello world"); } diff --git a/graphql-spring-boot-autoconfigure/src/test/resources/application.yml b/graphql-spring-boot-autoconfigure/src/test/resources/application.yml index 7c27c886..f8da4096 100644 --- a/graphql-spring-boot-autoconfigure/src/test/resources/application.yml +++ b/graphql-spring-boot-autoconfigure/src/test/resources/application.yml @@ -3,4 +3,8 @@ server: forward-headers-strategy: framework spring: autoconfigure: - exclude: org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration + exclude: + - org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration + - org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration + - org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration + - org.springframework.boot.actuate.autoconfigure.security.reactive.ReactiveManagementWebSecurityAutoConfiguration