diff --git a/integration-test/spring-boot-actuator-integration-tests/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/AbstractWebEndpointIntegrationTests.java b/integration-test/spring-boot-actuator-integration-tests/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/AbstractWebEndpointIntegrationTests.java index 87f74fc7b81..99bf9491152 100644 --- a/integration-test/spring-boot-actuator-integration-tests/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/AbstractWebEndpointIntegrationTests.java +++ b/integration-test/spring-boot-actuator-integration-tests/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/AbstractWebEndpointIntegrationTests.java @@ -162,15 +162,40 @@ public abstract class AbstractWebEndpointIntegrationTests client.get() + .uri("?a=alpha") + .exchange() + .expectStatus() + .isOk() + .expectBody() + .jsonPath("_links.length()") + .isEqualTo(3) + .jsonPath("_links.self.href") + .value(isLinkTo("/endpoints")) + .jsonPath("_links.self.templated") + .isEqualTo(false) + .jsonPath("_links.test.href") + .value(isLinkTo("/endpoints/test")) + .jsonPath("_links.test.templated") + .isEqualTo(false) + .jsonPath("_links.test-part.href") + .value(isLinkTo("/endpoints/test/{part}")) .jsonPath("_links.test-part.templated") .isEqualTo(true)); } @@ -668,6 +693,10 @@ public abstract class AbstractWebEndpointIntegrationTests client.get().uri("/customstatus").exchange().expectStatus().isEqualTo(234)); } + private Consumer isLinkTo(String target) { + return (href) -> assertThat(href).asString().doesNotContain("?").endsWith(target); + } + protected abstract int getPort(T context); protected void validateErrorBody(WebTestClient.BodyContentSpec body, HttpStatus status, String path, diff --git a/module/spring-boot-cloudfoundry/src/main/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/CloudFoundryWebFluxEndpointHandlerMapping.java b/module/spring-boot-cloudfoundry/src/main/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/CloudFoundryWebFluxEndpointHandlerMapping.java index 466e00df79b..2c269ca3207 100644 --- a/module/spring-boot-cloudfoundry/src/main/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/CloudFoundryWebFluxEndpointHandlerMapping.java +++ b/module/spring-boot-cloudfoundry/src/main/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/CloudFoundryWebFluxEndpointHandlerMapping.java @@ -50,6 +50,7 @@ import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.reactive.result.method.RequestMappingInfoHandlerMapping; import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.util.UriComponentsBuilder; /** * A custom {@link RequestMappingInfoHandlerMapping} that makes web endpoints available on @@ -111,8 +112,9 @@ class CloudFoundryWebFluxEndpointHandlerMapping extends AbstractWebFluxEndpointH return new ResponseEntity<>(securityResponse.getStatus()); } AccessLevel accessLevel = exchange.getAttribute(AccessLevel.REQUEST_ATTRIBUTE); + String requestUri = UriComponentsBuilder.fromUri(request.getURI()).replaceQuery(null).toUriString(); Map links = CloudFoundryWebFluxEndpointHandlerMapping.this.linksResolver - .resolveLinks(request.getURI().toString()); + .resolveLinks(requestUri); return new ResponseEntity<>( Collections.singletonMap("_links", getAccessibleLinks(accessLevel, links)), HttpStatus.OK); }); diff --git a/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/CloudFoundryWebFluxEndpointIntegrationTests.java b/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/CloudFoundryWebFluxEndpointIntegrationTests.java index 9c4634c3da7..b8798915a92 100644 --- a/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/CloudFoundryWebFluxEndpointIntegrationTests.java +++ b/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/reactive/CloudFoundryWebFluxEndpointIntegrationTests.java @@ -147,23 +147,23 @@ class CloudFoundryWebFluxEndpointIntegrationTests { .jsonPath("_links.length()") .isEqualTo(5) .jsonPath("_links.self.href") - .isNotEmpty() + .value(isLinkTo("/cfApplication")) .jsonPath("_links.self.templated") .isEqualTo(false) .jsonPath("_links.info.href") - .isNotEmpty() + .value(isLinkTo("/cfApplication/info")) .jsonPath("_links.info.templated") .isEqualTo(false) .jsonPath("_links.env.href") - .isNotEmpty() + .value(isLinkTo("/cfApplication/env")) .jsonPath("_links.env.templated") .isEqualTo(false) .jsonPath("_links.test.href") - .isNotEmpty() + .value(isLinkTo("/cfApplication/test")) .jsonPath("_links.test.templated") .isEqualTo(false) .jsonPath("_links.test-part.href") - .isNotEmpty() + .value(isLinkTo("/cfApplication/test/{part}")) .jsonPath("_links.test-part.templated") .isEqualTo(true))); } @@ -197,11 +197,11 @@ class CloudFoundryWebFluxEndpointIntegrationTests { .jsonPath("_links.length()") .isEqualTo(2) .jsonPath("_links.self.href") - .isNotEmpty() + .value(isLinkTo("/cfApplication")) .jsonPath("_links.self.templated") .isEqualTo(false) .jsonPath("_links.info.href") - .isNotEmpty() + .value(isLinkTo("/cfApplication/info")) .jsonPath("_links.info.templated") .isEqualTo(false) .jsonPath("_links.env") @@ -212,12 +212,34 @@ class CloudFoundryWebFluxEndpointIntegrationTests { .doesNotExist())); } + @Test + void whenRequestHasAQueryStringLinksToOtherEndpointsDoNotHaveAQueryString() { + given(this.tokenValidator.validate(any())).willReturn(Mono.empty()); + given(this.securityService.getAccessLevel(any(), eq("app-id"))).willReturn(Mono.just(AccessLevel.RESTRICTED)); + this.contextRunner.run(withWebTestClient((client) -> client.get() + .uri("/cfApplication?x=1") + .accept(MediaType.APPLICATION_JSON) + .header("Authorization", "bearer " + mockAccessToken()) + .exchange() + .expectStatus() + .isOk() + .expectBody() + .jsonPath("_links.self.href") + .value(isLinkTo("/cfApplication")) + .jsonPath("_links.info.href") + .value(isLinkTo("/cfApplication/info")))); + } + @Test void unknownEndpointsAreForbidden() { this.contextRunner.run(withWebTestClient( (client) -> client.get().uri("/cfApplication/unknown").exchange().expectStatus().isForbidden())); } + private Consumer isLinkTo(String target) { + return (href) -> assertThat(href).asString().doesNotContain("?").endsWith(target); + } + private ContextConsumer withWebTestClient( Consumer clientConsumer) { return (context) -> { diff --git a/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/CloudFoundryMvcWebEndpointIntegrationTests.java b/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/CloudFoundryMvcWebEndpointIntegrationTests.java index 29848f0a6cb..53256403c49 100644 --- a/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/CloudFoundryMvcWebEndpointIntegrationTests.java +++ b/module/spring-boot-cloudfoundry/src/test/java/org/springframework/boot/cloudfoundry/autoconfigure/actuate/endpoint/servlet/CloudFoundryMvcWebEndpointIntegrationTests.java @@ -135,23 +135,23 @@ class CloudFoundryMvcWebEndpointIntegrationTests { .jsonPath("_links.length()") .isEqualTo(5) .jsonPath("_links.self.href") - .isNotEmpty() + .value(isLinkTo("/cfApplication")) .jsonPath("_links.self.templated") .isEqualTo(false) .jsonPath("_links.info.href") - .isNotEmpty() + .value(isLinkTo("/cfApplication/info")) .jsonPath("_links.info.templated") .isEqualTo(false) .jsonPath("_links.env.href") - .isNotEmpty() + .value(isLinkTo("/cfApplication/env")) .jsonPath("_links.env.templated") .isEqualTo(false) .jsonPath("_links.test.href") - .isNotEmpty() + .value(isLinkTo("/cfApplication/test")) .jsonPath("_links.test.templated") .isEqualTo(false) .jsonPath("_links.test-part.href") - .isNotEmpty() + .value(isLinkTo("/cfApplication/test/{part}")) .jsonPath("_links.test-part.templated") .isEqualTo(true)); } @@ -186,11 +186,11 @@ class CloudFoundryMvcWebEndpointIntegrationTests { .jsonPath("_links.length()") .isEqualTo(2) .jsonPath("_links.self.href") - .isNotEmpty() + .value(isLinkTo("/cfApplication")) .jsonPath("_links.self.templated") .isEqualTo(false) .jsonPath("_links.info.href") - .isNotEmpty() + .value(isLinkTo("/cfApplication/info")) .jsonPath("_links.info.templated") .isEqualTo(false) .jsonPath("_links.env") @@ -201,6 +201,24 @@ class CloudFoundryMvcWebEndpointIntegrationTests { .doesNotExist()); } + @Test + void whenRequestHasAQueryStringLinksToOtherEndpointsDoNotHaveAQueryString() { + given(this.securityService.getAccessLevel(any(), eq("app-id"))).willReturn(AccessLevel.RESTRICTED); + load(TestEndpointConfiguration.class, + (client) -> client.get() + .uri("/cfApplication?x=1") + .accept(MediaType.APPLICATION_JSON) + .header("Authorization", "bearer " + mockAccessToken()) + .exchange() + .expectStatus() + .isOk() + .expectBody() + .jsonPath("_links.self.href") + .value(isLinkTo("/cfApplication")) + .jsonPath("_links.info.href") + .value(isLinkTo("/cfApplication/info"))); + } + @Test void unknownEndpointsAreForbidden() { load(TestEndpointConfiguration.class, @@ -212,6 +230,10 @@ class CloudFoundryMvcWebEndpointIntegrationTests { .isForbidden()); } + private Consumer isLinkTo(String target) { + return (href) -> assertThat(href).asString().doesNotContain("?").endsWith(target); + } + private void load(Class configuration, Consumer clientConsumer) { BiConsumer consumer = (context, client) -> clientConsumer.accept(client); new WebApplicationContextRunner(AnnotationConfigServletWebServerApplicationContext::new)