diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure-web/pom.xml b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure-web/pom.xml index b65a1d0a0..b83c5b51a 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure-web/pom.xml +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure-web/pom.xml @@ -10,7 +10,6 @@ org.springframework.cloud spring-cloud-function-adapter-parent 4.0.4-SNAPSHOT - ../.. UTF-8 diff --git a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/pom.xml b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/pom.xml index ce3fc1ec0..f0c5bfdce 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/pom.xml +++ b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/pom.xml @@ -11,7 +11,6 @@ org.springframework.cloud spring-cloud-function-adapter-parent 4.0.4-SNAPSHOT - ../.. UTF-8 diff --git a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/FunctionHttpProperties.java b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/FunctionHttpProperties.java new file mode 100644 index 000000000..66e527f99 --- /dev/null +++ b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/FunctionHttpProperties.java @@ -0,0 +1,86 @@ +/* + * Copyright 2023-2023 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.cloud.function.web; + +import java.util.List; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.function.context.FunctionProperties; + +/** +* +* @author Oleg Zhurakousky +* @since 4.0.4 +* +*/ + +@ConfigurationProperties(prefix = FunctionProperties.PREFIX + ".http") +public class FunctionHttpProperties { + + /** + * Blah. + */ + public List get; + + + /** + * Blah. + */ + public List post; + + /** + * Blah. + */ + public List put; + + /** + * Blah. + */ + public List delete; + + public List getGet() { + return this.get; + } + + public void setGet(List get) { + this.get = get; + } + + public List getPost() { + return post; + } + + public void setPost(List post) { + this.post = post; + } + + public List getPut() { + return put; + } + + public void setPut(List put) { + this.put = put; + } + + public List getDelete() { + return delete; + } + + public void setDelete(List delete) { + this.delete = delete; + } +} diff --git a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/FunctionController.java b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/FunctionController.java index 97b710ef7..42f64a745 100644 --- a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/FunctionController.java +++ b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/FunctionController.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2016-2023 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. @@ -20,7 +20,9 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper; +import org.springframework.cloud.function.web.FunctionHttpProperties; import org.springframework.cloud.function.web.constants.WebRequestConstants; import org.springframework.cloud.function.web.util.FunctionWebRequestProcessingHelper; import org.springframework.cloud.function.web.util.FunctionWrapper; @@ -32,8 +34,10 @@ import org.springframework.http.codec.multipart.Part; import org.springframework.stereotype.Component; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.server.ServerWebExchange; @@ -44,16 +48,29 @@ import org.springframework.web.server.ServerWebExchange; * @author Oleg Zhurakousky */ @Component +@EnableConfigurationProperties(FunctionHttpProperties.class) public class FunctionController { + private final FunctionHttpProperties functionHttpProperties; + + public FunctionController(FunctionHttpProperties functionHttpProperties) { + this.functionHttpProperties = functionHttpProperties; + } + @SuppressWarnings("unchecked") @PostMapping(path = "/**", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) @ResponseBody public Mono> form(ServerWebExchange request) { FunctionWrapper wrapper = wrapper(request); - return request.getFormData().doOnSuccess(params -> wrapper.getParams().addAll(params)) - .then(Mono.defer(() -> (Mono>) FunctionWebRequestProcessingHelper - .processRequest(wrapper, wrapper.getParams(), false))); + if (FunctionWebRequestProcessingHelper.isValidFunction("POST", wrapper.getFunction().getFunctionDefinition(), this.functionHttpProperties)) { + return request.getFormData().doOnSuccess(params -> wrapper.getParams().addAll(params)) + .then(Mono.defer(() -> (Mono>) FunctionWebRequestProcessingHelper + .processRequest(wrapper, wrapper.getParams(), false))); + } + else { + throw new IllegalArgumentException(FunctionWebRequestProcessingHelper.buildBadMappingErrorMessage("POST", wrapper.getFunction().getFunctionDefinition())); + } + } @SuppressWarnings("unchecked") @@ -61,10 +78,15 @@ public class FunctionController { @ResponseBody public Mono> multipart(ServerWebExchange request) { FunctionWrapper wrapper = wrapper(request); - return request.getMultipartData() - .doOnSuccess(params -> wrapper.getParams().addAll(multi(params))) - .then(Mono.defer(() -> (Mono>) FunctionWebRequestProcessingHelper - .processRequest(wrapper, wrapper.getParams(), false))); + if (FunctionWebRequestProcessingHelper.isValidFunction("POST", wrapper.getFunction().getFunctionDefinition(), this.functionHttpProperties)) { + return request.getMultipartData() + .doOnSuccess(params -> wrapper.getParams().addAll(multi(params))) + .then(Mono.defer(() -> (Mono>) FunctionWebRequestProcessingHelper + .processRequest(wrapper, wrapper.getParams(), false))); + } + else { + throw new IllegalArgumentException(FunctionWebRequestProcessingHelper.buildBadMappingErrorMessage("POST", wrapper.getFunction().getFunctionDefinition())); + } } @SuppressWarnings("unchecked") @@ -72,20 +94,66 @@ public class FunctionController { @ResponseBody public Mono> post(ServerWebExchange request, @RequestBody(required = false) String body) { - return (Mono>) FunctionWebRequestProcessingHelper.processRequest(wrapper(request), body, false); + FunctionWrapper wrapper = wrapper(request); + if (FunctionWebRequestProcessingHelper.isValidFunction("POST", wrapper.getFunction().getFunctionDefinition(), this.functionHttpProperties)) { + return (Mono>) FunctionWebRequestProcessingHelper.processRequest(wrapper, body, false); + } + else { + throw new IllegalArgumentException(FunctionWebRequestProcessingHelper.buildBadMappingErrorMessage("POST", wrapper.getFunction().getFunctionDefinition())); + } + } + + @SuppressWarnings("unchecked") + @PutMapping(path = "/**") + @ResponseBody + public Mono> put(ServerWebExchange request, + @RequestBody(required = false) String body) { + FunctionWrapper wrapper = wrapper(request); + if (FunctionWebRequestProcessingHelper.isValidFunction("PUT", wrapper.getFunction().getFunctionDefinition(), this.functionHttpProperties)) { + return (Mono>) FunctionWebRequestProcessingHelper.processRequest(wrapper, body, false); + } + else { + throw new IllegalArgumentException(FunctionWebRequestProcessingHelper.buildBadMappingErrorMessage("PUT", wrapper.getFunction().getFunctionDefinition())); + } + } + + @SuppressWarnings("unchecked") + @DeleteMapping(path = "/**") + @ResponseBody + public Mono> delete(ServerWebExchange request, + @RequestBody(required = false) String body) { + FunctionWrapper wrapper = wrapper(request); + if (FunctionWebRequestProcessingHelper.isValidFunction("DELETE", wrapper.getFunction().getFunctionDefinition(), this.functionHttpProperties)) { + return (Mono>) FunctionWebRequestProcessingHelper.processRequest(wrapper, body, false); + } + else { + throw new IllegalArgumentException(FunctionWebRequestProcessingHelper.buildBadMappingErrorMessage("DELETE", wrapper.getFunction().getFunctionDefinition())); + } } @PostMapping(path = "/**", produces = MediaType.TEXT_EVENT_STREAM_VALUE) @ResponseBody public Publisher postStream(ServerWebExchange request, @RequestBody(required = false) Flux body) { - return FunctionWebRequestProcessingHelper.processRequest(wrapper(request), body, true); + FunctionWrapper wrapper = wrapper(request); + if (FunctionWebRequestProcessingHelper.isValidFunction("POST", wrapper.getFunction().getFunctionDefinition(), this.functionHttpProperties)) { + return FunctionWebRequestProcessingHelper.processRequest(wrapper, body, true); + } + else { + throw new IllegalArgumentException(FunctionWebRequestProcessingHelper.buildBadMappingErrorMessage("POST", wrapper.getFunction().getFunctionDefinition())); + } + } @GetMapping(path = "/**", produces = MediaType.TEXT_EVENT_STREAM_VALUE) @ResponseBody public Publisher getStream(ServerWebExchange request) { FunctionWrapper wrapper = wrapper(request); - return FunctionWebRequestProcessingHelper.processRequest(wrapper, wrapper.getArgument(), true); + if (FunctionWebRequestProcessingHelper.isValidFunction("GET", wrapper.getFunction().getFunctionDefinition(), this.functionHttpProperties)) { + return FunctionWebRequestProcessingHelper.processRequest(wrapper, wrapper.getArgument(), true); + } + else { + throw new IllegalArgumentException(FunctionWebRequestProcessingHelper.buildBadMappingErrorMessage("GET", wrapper.getFunction().getFunctionDefinition())); + } } @SuppressWarnings("unchecked") @@ -93,7 +161,12 @@ public class FunctionController { @ResponseBody public Mono> get(ServerWebExchange request) { FunctionWrapper wrapper = wrapper(request); - return (Mono>) FunctionWebRequestProcessingHelper.processRequest(wrapper, wrapper.getArgument(), false); + if (FunctionWebRequestProcessingHelper.isValidFunction("GET", wrapper.getFunction().getFunctionDefinition(), this.functionHttpProperties)) { + return (Mono>) FunctionWebRequestProcessingHelper.processRequest(wrapper, wrapper.getArgument(), false); + } + else { + throw new IllegalArgumentException(FunctionWebRequestProcessingHelper.buildBadMappingErrorMessage("GET", wrapper.getFunction().getFunctionDefinition())); + } } private FunctionWrapper wrapper(ServerWebExchange request) { diff --git a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/mvc/FunctionController.java b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/mvc/FunctionController.java index f8de9c1ca..669ee2abd 100644 --- a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/mvc/FunctionController.java +++ b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/mvc/FunctionController.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2016-2023 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. @@ -25,7 +25,9 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper; +import org.springframework.cloud.function.web.FunctionHttpProperties; import org.springframework.cloud.function.web.constants.WebRequestConstants; import org.springframework.cloud.function.web.util.FunctionWebRequestProcessingHelper; import org.springframework.cloud.function.web.util.FunctionWrapper; @@ -39,8 +41,10 @@ import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.context.request.ServletWebRequest; @@ -54,34 +58,45 @@ import org.springframework.web.multipart.support.StandardMultipartHttpServletReq * @author Oleg Zhurakousky */ @Component +@EnableConfigurationProperties(FunctionHttpProperties.class) public class FunctionController { + private final FunctionHttpProperties functionHttpProperties; + + public FunctionController(FunctionHttpProperties functionHttpProperties) { + this.functionHttpProperties = functionHttpProperties; + } + @PostMapping(path = "/**", consumes = { MediaType.APPLICATION_FORM_URLENCODED_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE }) @ResponseBody public Object form(WebRequest request) { FunctionWrapper wrapper = wrapper(request); + if (FunctionWebRequestProcessingHelper.isValidFunction("POST", wrapper.getFunction().getFunctionDefinition(), this.functionHttpProperties)) { + if (((ServletWebRequest) request).getRequest() instanceof StandardMultipartHttpServletRequest) { + MultiValueMap multiFileMap = ((StandardMultipartHttpServletRequest) ((ServletWebRequest) request) + .getRequest()).getMultiFileMap(); + if (!CollectionUtils.isEmpty(multiFileMap)) { + List> files = multiFileMap.values().stream().flatMap(v -> v.stream()) + .map(file -> MessageBuilder.withPayload(file).copyHeaders(wrapper.getHeaders()).build()) + .collect(Collectors.toList()); + FunctionInvocationWrapper function = wrapper.getFunction(); - if (((ServletWebRequest) request).getRequest() instanceof StandardMultipartHttpServletRequest) { - MultiValueMap multiFileMap = ((StandardMultipartHttpServletRequest) ((ServletWebRequest) request) - .getRequest()).getMultiFileMap(); - if (!CollectionUtils.isEmpty(multiFileMap)) { - List> files = multiFileMap.values().stream().flatMap(v -> v.stream()) - .map(file -> MessageBuilder.withPayload(file).copyHeaders(wrapper.getHeaders()).build()) - .collect(Collectors.toList()); - FunctionInvocationWrapper function = wrapper.getFunction(); - - Publisher result = (Publisher) function.apply(Flux.fromIterable(files)); - BodyBuilder builder = ResponseEntity.ok(); - if (result instanceof Flux) { - result = Flux.from(result).map(message -> { - return message instanceof Message ? ((Message) message).getPayload() : message; - }).collectList(); + Publisher result = (Publisher) function.apply(Flux.fromIterable(files)); + BodyBuilder builder = ResponseEntity.ok(); + if (result instanceof Flux) { + result = Flux.from(result).map(message -> { + return message instanceof Message ? ((Message) message).getPayload() : message; + }).collectList(); + } + return Mono.from(result).flatMap(body -> Mono.just(builder.body(body))); } - return Mono.from(result).flatMap(body -> Mono.just(builder.body(body))); } + return FunctionWebRequestProcessingHelper.processRequest(wrapper, wrapper.getParams(), false); + } + else { + throw new IllegalArgumentException(FunctionWebRequestProcessingHelper.buildBadMappingErrorMessage("POST", wrapper.getFunction().getFunctionDefinition())); } - return FunctionWebRequestProcessingHelper.processRequest(wrapper, wrapper.getParams(), false); } @SuppressWarnings("unchecked") @@ -90,29 +105,74 @@ public class FunctionController { public Mono>> postStream(WebRequest request, @RequestBody(required = false) String body) { String argument = StringUtils.hasText(body) ? body : ""; - return ((Mono>) FunctionWebRequestProcessingHelper.processRequest(wrapper(request), argument, true)).map(response -> ResponseEntity.ok() - .headers(response.getHeaders()).body((Publisher) response.getBody())); + FunctionWrapper wrapper = wrapper(request); + if (FunctionWebRequestProcessingHelper.isValidFunction("POST", wrapper.getFunction().getFunctionDefinition(), this.functionHttpProperties)) { + return ((Mono>) FunctionWebRequestProcessingHelper.processRequest(wrapper, argument, true)).map(response -> ResponseEntity.ok() + .headers(response.getHeaders()).body((Publisher) response.getBody())); + } + else { + throw new IllegalArgumentException(FunctionWebRequestProcessingHelper.buildBadMappingErrorMessage("POST", wrapper.getFunction().getFunctionDefinition())); + } } @GetMapping(path = "/**", produces = MediaType.TEXT_EVENT_STREAM_VALUE) @ResponseBody public Publisher getStream(WebRequest request) { FunctionWrapper wrapper = wrapper(request); - return FunctionWebRequestProcessingHelper - .processRequest(wrapper, wrapper.getArgument(), true); + if (FunctionWebRequestProcessingHelper.isValidFunction("GET", wrapper.getFunction().getFunctionDefinition(), this.functionHttpProperties)) { + return FunctionWebRequestProcessingHelper.processRequest(wrapper, wrapper.getArgument(), true); + } + else { + throw new IllegalArgumentException(FunctionWebRequestProcessingHelper.buildBadMappingErrorMessage("GET", wrapper.getFunction().getFunctionDefinition())); + } } @PostMapping(path = "/**") @ResponseBody public Object post(WebRequest request, @RequestBody(required = false) String body) { - return FunctionWebRequestProcessingHelper.processRequest(wrapper(request), body, false); + FunctionWrapper wrapper = wrapper(request); + if (FunctionWebRequestProcessingHelper.isValidFunction("POST", wrapper.getFunction().getFunctionDefinition(), this.functionHttpProperties)) { + return FunctionWebRequestProcessingHelper.processRequest(wrapper, body, false); + } + else { + throw new IllegalArgumentException(FunctionWebRequestProcessingHelper.buildBadMappingErrorMessage("POST", wrapper.getFunction().getFunctionDefinition())); + } + } + + @PutMapping(path = "/**") + @ResponseBody + public Object put(WebRequest request, @RequestBody(required = false) String body) { + FunctionWrapper wrapper = wrapper(request); + if (FunctionWebRequestProcessingHelper.isValidFunction("PUT", wrapper.getFunction().getFunctionDefinition(), this.functionHttpProperties)) { + return FunctionWebRequestProcessingHelper.processRequest(wrapper, body, false); + } + else { + throw new IllegalArgumentException(FunctionWebRequestProcessingHelper.buildBadMappingErrorMessage("PUT", wrapper.getFunction().getFunctionDefinition())); + } + } + + @DeleteMapping(path = "/**") + @ResponseBody + public Object delete(WebRequest request, @RequestBody(required = false) String body) { + FunctionWrapper wrapper = wrapper(request); + if (FunctionWebRequestProcessingHelper.isValidFunction("DELETE", wrapper.getFunction().getFunctionDefinition(), this.functionHttpProperties)) { + return FunctionWebRequestProcessingHelper.processRequest(wrapper, body, false); + } + else { + throw new IllegalArgumentException(FunctionWebRequestProcessingHelper.buildBadMappingErrorMessage("DELETE", wrapper.getFunction().getFunctionDefinition())); + } } @GetMapping(path = "/**") @ResponseBody public Object get(WebRequest request) { FunctionWrapper wrapper = wrapper(request); - return FunctionWebRequestProcessingHelper.processRequest(wrapper, wrapper.getArgument(), false); + if (FunctionWebRequestProcessingHelper.isValidFunction("GET", wrapper.getFunction().getFunctionDefinition(), this.functionHttpProperties)) { + return FunctionWebRequestProcessingHelper.processRequest(wrapper, wrapper.getArgument(), false); + } + else { + throw new IllegalArgumentException(FunctionWebRequestProcessingHelper.buildBadMappingErrorMessage("GET", wrapper.getFunction().getFunctionDefinition())); + } } private FunctionWrapper wrapper(WebRequest request) { diff --git a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/util/FunctionWebRequestProcessingHelper.java b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/util/FunctionWebRequestProcessingHelper.java index ebc1795f9..8a7d37d6e 100644 --- a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/util/FunctionWebRequestProcessingHelper.java +++ b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/util/FunctionWebRequestProcessingHelper.java @@ -30,6 +30,7 @@ import reactor.core.publisher.Mono; import org.springframework.cloud.function.context.FunctionCatalog; import org.springframework.cloud.function.context.FunctionProperties; import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper; +import org.springframework.cloud.function.web.FunctionHttpProperties; import org.springframework.cloud.function.web.constants.WebRequestConstants; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -71,6 +72,35 @@ public final class FunctionWebRequestProcessingHelper { return postProcessResult(result, isMessage); } + public static boolean isValidFunction(String httpMethod, String functionDefinition, FunctionHttpProperties functionHttpProperties) { + List functionDefinitions = null; + switch (httpMethod) { + case "GET": + functionDefinitions = functionHttpProperties.getGet(); + break; + case "POST": + functionDefinitions = functionHttpProperties.getPost(); + break; + case "PUT": + functionDefinitions = functionHttpProperties.getPut(); + break; + case "DELETE": + functionDefinitions = functionHttpProperties.getDelete(); + break; + default: + return false; + } + return CollectionUtils.isEmpty(functionDefinitions) || functionDefinitions.contains(functionDefinition); + } + + public static String buildBadMappingErrorMessage(String httpMethod, String functionDefinition) { + return "Function '" + functionDefinition + "' is not eligible to be invoked " + + "via " + httpMethod + " method. This is due to the fact that explicit mappings for " + httpMethod + + " are provided via 'spring.cloud.function.http." + httpMethod + "' property " + + "and this function is not listed there. Either remove all explicit mappings for " + httpMethod + " or add this function to the list of functions " + + "specified in 'spring.cloud.function.http." + httpMethod + "' property."; + } + @SuppressWarnings({ "rawtypes", "unchecked" }) public static Publisher processRequest(FunctionWrapper wrapper, Object argument, boolean eventStream) { if (argument == null) {