relates to gh-600 Add Predicate and Filter validation to Route… (#1212)

* relates to gh-600 Add Predicate and Filter validation to Route Creation Actuator Endpoint

* renamed a variable in validateRouteDefinition(RouteDefinition) to better fit the injected parameter
This commit is contained in:
Dennis Menge
2019-09-16 18:30:45 +02:00
committed by Spencer Gibb
parent 50632cba6b
commit 29296eda41
5 changed files with 186 additions and 16 deletions

View File

@@ -28,6 +28,7 @@ import reactor.core.publisher.Mono;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory;
import org.springframework.cloud.gateway.handler.predicate.RoutePredicateFactory;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionLocator;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
@@ -56,6 +57,8 @@ public class AbstractGatewayControllerEndpoint implements ApplicationEventPublis
protected List<GatewayFilterFactory> GatewayFilters;
protected List<RoutePredicateFactory> routePredicates;
protected RouteDefinitionWriter routeDefinitionWriter;
protected RouteLocator routeLocator;
@@ -65,10 +68,12 @@ public class AbstractGatewayControllerEndpoint implements ApplicationEventPublis
public AbstractGatewayControllerEndpoint(
RouteDefinitionLocator routeDefinitionLocator,
List<GlobalFilter> globalFilters, List<GatewayFilterFactory> GatewayFilters,
List<RoutePredicateFactory> routePredicates,
RouteDefinitionWriter routeDefinitionWriter, RouteLocator routeLocator) {
this.routeDefinitionLocator = routeDefinitionLocator;
this.globalFilters = globalFilters;
this.GatewayFilters = GatewayFilters;
this.routePredicates = routePredicates;
this.routeDefinitionWriter = routeDefinitionWriter;
this.routeLocator = routeLocator;
}
@@ -96,6 +101,11 @@ public class AbstractGatewayControllerEndpoint implements ApplicationEventPublis
return getNamesToOrders(this.GatewayFilters);
}
@GetMapping("/routepredicates")
public Mono<HashMap<String, Object>> routepredicates() {
return getNamesToOrders(this.routePredicates);
}
private <T> Mono<HashMap<String, Object>> getNamesToOrders(List<T> list) {
return Flux.fromIterable(list).reduce(new HashMap<>(), this::putItem);
}
@@ -117,14 +127,35 @@ public class AbstractGatewayControllerEndpoint implements ApplicationEventPublis
*/
@PostMapping("/routes/{id}")
@SuppressWarnings("unchecked")
public Mono<ResponseEntity<Void>> save(@PathVariable String id,
@RequestBody Mono<RouteDefinition> route) {
return this.routeDefinitionWriter.save(route.map(r -> {
r.setId(id);
log.debug("Saving route: " + route);
return r;
})).then(Mono.defer(() -> Mono
.just(ResponseEntity.created(URI.create("/routes/" + id)).build())));
public Mono<ResponseEntity<Object>> save(@PathVariable String id,
@RequestBody RouteDefinition route) {
return Mono.just(route).filter(this::validateRouteDefinition)
.flatMap(routeDefinition -> this.routeDefinitionWriter
.save(Mono.just(routeDefinition).map(r -> {
r.setId(id);
log.debug("Saving route: " + route);
return r;
}))
.then(Mono.defer(() -> Mono.just(ResponseEntity
.created(URI.create("/routes/" + id)).build()))))
.switchIfEmpty(
Mono.defer(() -> Mono.just(ResponseEntity.badRequest().build())));
}
private boolean validateRouteDefinition(RouteDefinition routeDefinition) {
boolean hasValidFilterDefinitions = routeDefinition.getFilters().stream()
.allMatch(filterDefinition -> GatewayFilters.stream()
.anyMatch(gatewayFilterFactory -> filterDefinition.getName()
.equals(gatewayFilterFactory.name())));
boolean hasValidPredicateDefinitions = routeDefinition.getPredicates().stream()
.allMatch(predicateDefinition -> routePredicates.stream()
.anyMatch(routePredicate -> predicateDefinition.getName()
.equals(routePredicate.name())));
log.debug("FilterDefinitions valid: " + hasValidFilterDefinitions);
log.debug("PredicateDefinitions valid: " + hasValidPredicateDefinitions);
return hasValidFilterDefinitions && hasValidPredicateDefinitions;
}
@DeleteMapping("/routes/{id}")

View File

@@ -28,6 +28,7 @@ import org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEn
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory;
import org.springframework.cloud.gateway.handler.predicate.RoutePredicateFactory;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.cloud.gateway.route.RouteLocator;
@@ -43,8 +44,10 @@ public class GatewayControllerEndpoint extends AbstractGatewayControllerEndpoint
public GatewayControllerEndpoint(List<GlobalFilter> globalFilters,
List<GatewayFilterFactory> gatewayFilters,
List<RoutePredicateFactory> routePredicates,
RouteDefinitionWriter routeDefinitionWriter, RouteLocator routeLocator) {
super(null, globalFilters, gatewayFilters, routeDefinitionWriter, routeLocator);
super(null, globalFilters, gatewayFilters, routePredicates, routeDefinitionWriter,
routeLocator);
}
// TODO: Flush out routes without a definition

View File

@@ -27,6 +27,7 @@ import org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEn
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory;
import org.springframework.cloud.gateway.handler.predicate.RoutePredicateFactory;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionLocator;
@@ -44,8 +45,9 @@ public class GatewayLegacyControllerEndpoint extends AbstractGatewayControllerEn
public GatewayLegacyControllerEndpoint(RouteDefinitionLocator routeDefinitionLocator,
List<GlobalFilter> globalFilters, List<GatewayFilterFactory> GatewayFilters,
List<RoutePredicateFactory> routePredicates,
RouteDefinitionWriter routeDefinitionWriter, RouteLocator routeLocator) {
super(routeDefinitionLocator, globalFilters, GatewayFilters,
super(routeDefinitionLocator, globalFilters, GatewayFilters, routePredicates,
routeDefinitionWriter, routeLocator);
}

View File

@@ -688,8 +688,9 @@ public class GatewayAutoConfiguration {
public GatewayControllerEndpoint gatewayControllerEndpoint(
List<GlobalFilter> globalFilters,
List<GatewayFilterFactory> gatewayFilters,
List<RoutePredicateFactory> routePredicates,
RouteDefinitionWriter routeDefinitionWriter, RouteLocator routeLocator) {
return new GatewayControllerEndpoint(globalFilters, gatewayFilters,
return new GatewayControllerEndpoint(globalFilters, gatewayFilters, routePredicates,
routeDefinitionWriter, routeLocator);
}
@@ -700,9 +701,10 @@ public class GatewayAutoConfiguration {
RouteDefinitionLocator routeDefinitionLocator,
List<GlobalFilter> globalFilters,
List<GatewayFilterFactory> gatewayFilters,
List<RoutePredicateFactory> routePredicates,
RouteDefinitionWriter routeDefinitionWriter, RouteLocator routeLocator) {
return new GatewayLegacyControllerEndpoint(routeDefinitionLocator,
globalFilters, gatewayFilters, routeDefinitionWriter, routeLocator);
globalFilters, gatewayFilters, routePredicates, routeDefinitionWriter, routeLocator);
}
}

View File

@@ -16,8 +16,12 @@
package org.springframework.cloud.gateway.actuate;
import java.net.URI;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -27,20 +31,30 @@ import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.cloud.gateway.filter.FilterDefinition;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.GatewayPredicate;
import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.cloud.gateway.test.PermitAllSecurityConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.server.ServerWebExchange;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
@RunWith(SpringRunner.class)
@SpringBootTest(properties = { "management.endpoints.web.exposure.include=*",
"spring.cloud.gateway.actuator.verbose.enabled=true" }, webEnvironment = RANDOM_PORT)
@SpringBootTest(properties = {"management.endpoints.web.exposure.include=*",
"spring.cloud.gateway.actuator.verbose.enabled=true"}, webEnvironment = RANDOM_PORT)
public class GatewayControllerEndpointTests {
@Autowired
@@ -78,6 +92,92 @@ public class GatewayControllerEndpointTests {
});
}
@Test
public void testRouteFilters() {
testClient.get()
.uri("http://localhost:" + port + "/actuator/gateway/routefilters")
.exchange().expectStatus().isOk().expectBody(Map.class)
.consumeWith(result -> {
Map<?, ?> responseBody = result.getResponseBody();
assertThat(responseBody).isNotEmpty();
});
}
@Test
public void testRoutePredicates() {
testClient.get()
.uri("http://localhost:" + port + "/actuator/gateway/routepredicates")
.exchange().expectStatus().isOk().expectBody(Map.class)
.consumeWith(result -> {
Map<?, ?> responseBody = result.getResponseBody();
assertThat(responseBody).isNotEmpty();
});
}
@Test
public void testPostValidRouteDefinition() {
RouteDefinition testRouteDefinition = new RouteDefinition();
testRouteDefinition.setUri(URI.create("http://example.org"));
FilterDefinition prefixPathFilterDefinition = new FilterDefinition(
"PrefixPath=/test-path");
FilterDefinition redirectToFilterDefinition = new FilterDefinition(
"RemoveResponseHeader=Sensitive-Header");
FilterDefinition testFilterDefinition = new FilterDefinition("TestFilter");
testRouteDefinition.setFilters(Arrays.asList(prefixPathFilterDefinition,
redirectToFilterDefinition, testFilterDefinition));
PredicateDefinition hostRoutePredicateDefinition = new PredicateDefinition(
"Host=myhost.org");
PredicateDefinition methodRoutePredicateDefinition = new PredicateDefinition(
"Method=GET");
PredicateDefinition testPredicateDefinition = new PredicateDefinition(
"Test=value");
testRouteDefinition.setPredicates(Arrays.asList(hostRoutePredicateDefinition,
methodRoutePredicateDefinition, testPredicateDefinition));
testClient.post()
.uri("http://localhost:" + port + "/actuator/gateway/routes/test-route")
.accept(MediaType.APPLICATION_JSON_UTF8)
.body(BodyInserters.fromObject(testRouteDefinition)).exchange()
.expectStatus().isCreated();
}
@Test
public void testPostRouteWithNotExistingFilter() {
RouteDefinition testRouteDefinition = new RouteDefinition();
testRouteDefinition.setUri(URI.create("http://example.org"));
FilterDefinition filterDefinition = new FilterDefinition(
"NotExistingFilter=test-config");
testRouteDefinition.setFilters(Collections.singletonList(filterDefinition));
testClient.post()
.uri("http://localhost:" + port + "/actuator/gateway/routes/test-route")
.accept(MediaType.APPLICATION_JSON_UTF8)
.body(BodyInserters.fromObject(testRouteDefinition)).exchange()
.expectStatus().isBadRequest();
}
@Test
public void testPostRouteWithNotExistingPredicate() {
RouteDefinition testRouteDefinition = new RouteDefinition();
testRouteDefinition.setUri(URI.create("http://example.org"));
PredicateDefinition predicateDefinition = new PredicateDefinition(
"NotExistingPredicate=test-config");
testRouteDefinition.setPredicates(Collections.singletonList(predicateDefinition));
testClient.post()
.uri("http://localhost:" + port + "/actuator/gateway/routes/test-route")
.accept(MediaType.APPLICATION_JSON_UTF8)
.body(BodyInserters.fromObject(testRouteDefinition)).exchange()
.expectStatus().isBadRequest();
}
@SpringBootConfiguration
@EnableAutoConfiguration
@Import(PermitAllSecurityConfiguration.class)
@@ -86,11 +186,43 @@ public class GatewayControllerEndpointTests {
@Bean
RouteLocator testRouteLocator(RouteLocatorBuilder routeLocatorBuilder) {
return routeLocatorBuilder.routes()
.route("test-service",
r -> r.path("/test-service/**").uri("lb://test-service"))
.route("test-service", r -> r.path("/test-service/**").uri("lb://test-service"))
.build();
}
@Bean
public TestFilterGatewayFilterFactory customGatewayFilterFactory() {
return new TestFilterGatewayFilterFactory();
}
@Bean
public TestRoutePredicateFactory customGatewayPredicateFactory() {
return new TestRoutePredicateFactory(Object.class);
}
}
private static class TestFilterGatewayFilterFactory
extends AbstractGatewayFilterFactory {
@Override
public GatewayFilter apply(Object config) {
return null;
}
}
private static class TestRoutePredicateFactory extends AbstractRoutePredicateFactory {
TestRoutePredicateFactory(Class configClass) {
super(configClass);
}
@Override
public Predicate<ServerWebExchange> apply(Object config) {
return (GatewayPredicate) serverWebExchange -> true;
}
}
}