From: Kai Moritz Date: Sun, 9 Apr 2023 18:41:49 +0000 (+0200) Subject: Implemented an interceptor, that forbiddes access according to a header X-Git-Url: http://juplo.de/gitweb/?a=commitdiff_plain;h=280599c77a6ae267a544236bdc1e8eb5172b14e7;p=demos%2Fspring-boot Implemented an interceptor, that forbiddes access according to a header --- diff --git a/src/main/java/de/juplo/demo/BackendVersionInterceptor.java b/src/main/java/de/juplo/demo/BackendVersionInterceptor.java deleted file mode 100644 index f8a046b..0000000 --- a/src/main/java/de/juplo/demo/BackendVersionInterceptor.java +++ /dev/null @@ -1,38 +0,0 @@ -package de.juplo.demo; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import org.springframework.web.servlet.HandlerInterceptor; - -import java.io.IOException; - - -@RequiredArgsConstructor -public class BackendVersionInterceptor implements HandlerInterceptor -{ - public final static String BACKEND_VERSION = "X-Backend-Version"; - - - private final String backendVersion; - - - @Override - public boolean preHandle( - HttpServletRequest request, - HttpServletResponse response, - Object handler) throws IOException - { - String requstedVersion = request.getHeader(BACKEND_VERSION); - if (requstedVersion != null && !requstedVersion.equals(backendVersion)) - { - response.sendError(HttpStatus.GONE.value()); - return false; - } - else - { - return true; - } - } -} diff --git a/src/main/java/de/juplo/demo/DemoApplication.java b/src/main/java/de/juplo/demo/DemoApplication.java index 64e04c8..c593c19 100644 --- a/src/main/java/de/juplo/demo/DemoApplication.java +++ b/src/main/java/de/juplo/demo/DemoApplication.java @@ -15,13 +15,6 @@ public class DemoApplication String projectVersion; - @Bean - public BackendVersionInterceptor backendVersionInterceptor( - @Value("${build.version}") String projectVersion) - { - return new BackendVersionInterceptor(projectVersion); - } - @Bean public String from( @Value("${application.name}") String applicationName, diff --git a/src/main/java/de/juplo/demo/DemoRestController.java b/src/main/java/de/juplo/demo/DemoRestController.java index 4259a30..ed364a0 100644 --- a/src/main/java/de/juplo/demo/DemoRestController.java +++ b/src/main/java/de/juplo/demo/DemoRestController.java @@ -17,8 +17,9 @@ public class DemoRestController private final String from; - @PostMapping("{to}") - public GreetingTO greet( + @RequestableByHumans + @PostMapping("/greet/{to}") + public MessageTo greet( @PathVariable String to, @RequestBody String greeting) { @@ -29,6 +30,21 @@ public class DemoRestController log.info("Greeting from {} to {}: {}", from, to, message); - return GreetingTO.of(message, from, to); + return MessageTo.of(message, from, to); + } + + @PostMapping("/acknowledge/{to}") + public MessageTo acknowledge( + @PathVariable String to, + @RequestBody String acknowledgment) + { + String message = acknowledgment + .replaceAll(PLACEHOLDER_FROM, from) + .replaceAll(PLACEHOLDER_TO, to) + .trim(); + + log.info("Acknowledgement from {} to {}: {}", from, to, message); + + return MessageTo.of(message, from, to); } } diff --git a/src/main/java/de/juplo/demo/DemoWebMvcConfigurer.java b/src/main/java/de/juplo/demo/DemoWebMvcConfigurer.java index 35fa5aa..aae2562 100644 --- a/src/main/java/de/juplo/demo/DemoWebMvcConfigurer.java +++ b/src/main/java/de/juplo/demo/DemoWebMvcConfigurer.java @@ -10,12 +10,12 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; public class DemoWebMvcConfigurer implements WebMvcConfigurer { @Autowired - BackendVersionInterceptor backendVersionInterceptor; + RequestTypeInterceptor requestTypeInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(backendVersionInterceptor); + registry.addInterceptor(requestTypeInterceptor); } } diff --git a/src/main/java/de/juplo/demo/GreetingTO.java b/src/main/java/de/juplo/demo/GreetingTO.java deleted file mode 100644 index c61a30e..0000000 --- a/src/main/java/de/juplo/demo/GreetingTO.java +++ /dev/null @@ -1,21 +0,0 @@ -package de.juplo.demo; - - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.AllArgsConstructor; -import lombok.Data; - -import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY; - - -@Data -@AllArgsConstructor(staticName = "of") -@JsonIgnoreProperties(ignoreUnknown = true) -@JsonInclude(NON_EMPTY) -public class GreetingTO -{ - private String greeting; - private String from; - private String to; -} diff --git a/src/main/java/de/juplo/demo/MessageTo.java b/src/main/java/de/juplo/demo/MessageTo.java new file mode 100644 index 0000000..c024dc1 --- /dev/null +++ b/src/main/java/de/juplo/demo/MessageTo.java @@ -0,0 +1,21 @@ +package de.juplo.demo; + + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AllArgsConstructor; +import lombok.Data; + +import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY; + + +@Data +@AllArgsConstructor(staticName = "of") +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(NON_EMPTY) +public class MessageTo +{ + private String message; + private String from; + private String to; +} diff --git a/src/main/java/de/juplo/demo/RequestTypeInterceptor.java b/src/main/java/de/juplo/demo/RequestTypeInterceptor.java new file mode 100644 index 0000000..fa1f428 --- /dev/null +++ b/src/main/java/de/juplo/demo/RequestTypeInterceptor.java @@ -0,0 +1,88 @@ +package de.juplo.demo; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; + +import java.io.IOException; + + +@Component +@Slf4j +public class RequestTypeInterceptor implements HandlerInterceptor +{ + public final static String REQUEST_TYPE = "X-Request-Type"; + + public enum RequestType { HUMAN, MACHINE }; + + + @Override + public boolean preHandle( + HttpServletRequest request, + HttpServletResponse response, + Object handler) throws IOException + { + // Only instances of HandlerMethod are intercepted for the type of the request + if (!(handler instanceof HandlerMethod)) + { + log.trace("Ignoring handler of type {}", handler); + return true; + } + + String requestTypeHeader = request.getHeader(REQUEST_TYPE); + if (requestTypeHeader == null) + { + sendBadRequest(response, "Ignoring request without header"); + return false; + } + + try + { + RequestType requestType = RequestType.valueOf(requestTypeHeader.toUpperCase()); + + if (requestType == RequestType.MACHINE) + { + log.debug("Accepting machine-request for {}", handler); + return true; + } + + if (((HandlerMethod)handler).getMethod().getAnnotation(RequestableByHumans.class) != null) + { + log.info("Accepting human request for {}", handler); + return true; + } + else + { + sendForbidden(response, "Human requests are not allowed for " + handler); + return false; + } + } + catch (IllegalArgumentException e) + { + log.warn("Invalid RequestType: {}", e.toString()); + } + + sendBadRequest(response, requestTypeHeader); + return false; + } + + void sendForbidden( + HttpServletResponse response, + String message) throws IOException + { + log.warn("Forbidden: {}", message); + response.sendError(HttpStatus.FORBIDDEN.value(), message); + } + + void sendBadRequest( + HttpServletResponse response, + String message) throws IOException + { + log.warn("Bad request: {}", message); + response.sendError(HttpStatus.BAD_REQUEST.value(), message); + } +} diff --git a/src/main/java/de/juplo/demo/RequestableByHumans.java b/src/main/java/de/juplo/demo/RequestableByHumans.java new file mode 100644 index 0000000..d5f33c4 --- /dev/null +++ b/src/main/java/de/juplo/demo/RequestableByHumans.java @@ -0,0 +1,14 @@ +package de.juplo.demo; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface RequestableByHumans +{ +} diff --git a/src/test/java/de/juplo/demo/DemoApplicationTests.java b/src/test/java/de/juplo/demo/DemoApplicationTests.java index 576544f..0c07e93 100644 --- a/src/test/java/de/juplo/demo/DemoApplicationTests.java +++ b/src/test/java/de/juplo/demo/DemoApplicationTests.java @@ -7,7 +7,9 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMock import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.web.servlet.MockMvc; -import static de.juplo.demo.BackendVersionInterceptor.BACKEND_VERSION; +import static de.juplo.demo.RequestTypeInterceptor.REQUEST_TYPE; +import static de.juplo.demo.RequestTypeInterceptor.RequestType.HUMAN; +import static de.juplo.demo.RequestTypeInterceptor.RequestType.MACHINE; import static org.springframework.http.HttpHeaders.ACCEPT; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -31,60 +33,95 @@ class DemoApplicationTests @Test - void getWelcomeResponseIsOkIfBackendHeaderIsNotSet(@Autowired MockMvc mvc) throws Exception + void getWelcomeResponseIsOkIfRequestTypeHeaderIsNotSet(@Autowired MockMvc mvc) throws Exception { - mvc.perform(get("/")).andExpect(status().isOk()); + mvc + .perform(get("/")) + .andExpect(status().isOk()); } @Test - void getWelcomeResponseIsOkIfBackendHeaderDoesMatch(@Autowired MockMvc mvc) throws Exception + void getWelcomeResponseIsOkIfRequestIsOfTypeMachine(@Autowired MockMvc mvc) throws Exception { mvc .perform(get("/") - .header(BACKEND_VERSION, projectVersion)) + .header(REQUEST_TYPE, MACHINE)) .andExpect(status().isOk()); } @Test - void getWelcomeResponseIsGoneIfBackendHeaderDoesNotMatch(@Autowired MockMvc mvc) throws Exception + void getWelcomeResponseIsOkIfRequestIsOfTypeHuman(@Autowired MockMvc mvc) throws Exception { mvc .perform(get("/") - .header(BACKEND_VERSION, "FOO")) - .andExpect(status().isGone()); + .header(REQUEST_TYPE, HUMAN)) + .andExpect(status().isOk()); } @Test - void getGreetingIfBackendHeaderIsNotSet(@Autowired MockMvc mvc) throws Exception + void expectedResultsForGreetIfRequestIsOfTypeMachine(@Autowired MockMvc mvc) throws Exception { mvc - .perform(post("/peter") + .perform(post("/greet/peter") .header(ACCEPT, "application/json") - .header(BACKEND_VERSION, projectVersion) + .header(REQUEST_TYPE, MACHINE) .content("Hello TO_NAME, nice to meet you. I'm FROM_NAME.")) .andExpect(status().isOk()) - .andExpect(content().json("{\"greeting\": \"Hello peter, nice to meet you. I'm FOO@BAR.\"}")); + .andExpect(content().json("{\"message\": \"Hello peter, nice to meet you. I'm FOO@BAR.\"}")); } @Test - void getGreetingIfBackendHeaderDoesMatch(@Autowired MockMvc mvc) throws Exception + void expectedResultsForGreetIfRequestIsOfTypeHuman(@Autowired MockMvc mvc) throws Exception { mvc - .perform(post("/peter") + .perform(post("/greet/peter") .header(ACCEPT, "application/json") - .header(BACKEND_VERSION, "FOO") + .header(REQUEST_TYPE, HUMAN) .content("Hello TO_NAME, nice to meet you. I'm FROM_NAME.")) - .andExpect(status().isGone()); + .andExpect(status().isOk()) + .andExpect(content().json("{\"message\": \"Hello peter, nice to meet you. I'm FOO@BAR.\"}")); } @Test - void getGreetingIfBackendHeaderDoesNotMatch(@Autowired MockMvc mvc) throws Exception + void badRequestForGreetIfRequestTypeHeaderIsNotSet(@Autowired MockMvc mvc) throws Exception { mvc - .perform(post("/peter") + .perform(post("/greet/peter") .header(ACCEPT, "application/json") .content("Hello TO_NAME, nice to meet you. I'm FROM_NAME.")) + .andExpect(status().isBadRequest()); + } + + @Test + void expectedResultsForAcknowlegeIfRequestIsOfTypeMachine(@Autowired MockMvc mvc) throws Exception + { + mvc + .perform(post("/acknowledge/peter") + .header(ACCEPT, "application/json") + .header(REQUEST_TYPE, MACHINE) + .content("Hello TO_NAME, nice to meet you. I'm FROM_NAME.")) .andExpect(status().isOk()) - .andExpect(content().json("{\"greeting\": \"Hello peter, nice to meet you. I'm FOO@BAR.\"}")); + .andExpect(content().json("{\"message\": \"Hello peter, nice to meet you. I'm FOO@BAR.\"}")); + } + + @Test + void isForbiddenForAcknowlegeIfRequestIsOfTypeHuman(@Autowired MockMvc mvc) throws Exception + { + mvc + .perform(post("/acknowledge/peter") + .header(ACCEPT, "application/json") + .header(REQUEST_TYPE, HUMAN) + .content("Hello TO_NAME, nice to meet you. I'm FROM_NAME.")) + .andExpect(status().isForbidden()); + } + + @Test + void badRequestForAcknowlegeIfRequestTypeHeaderIsNotSet(@Autowired MockMvc mvc) throws Exception + { + mvc + .perform(post("/acknowledge/peter") + .header(ACCEPT, "application/json") + .content("Hello TO_NAME, nice to meet you. I'm FROM_NAME.")) + .andExpect(status().isBadRequest()); } }