From f50f05b57e1dfdeb5bda3b8850fed3ab09074e85 Mon Sep 17 00:00:00 2001 From: birddevelper Date: Tue, 24 Dec 2024 22:44:03 +0200 Subject: [PATCH 1/7] fix: Separate error response --- .../exceptions/UnauthorizedException.java | 4 ++-- .../springrestframework/web/responses/ErrorResponse.java | 5 ----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/main/java/io/github/nikanique/springrestframework/exceptions/UnauthorizedException.java b/src/main/java/io/github/nikanique/springrestframework/exceptions/UnauthorizedException.java index dab33ac..8f9a060 100644 --- a/src/main/java/io/github/nikanique/springrestframework/exceptions/UnauthorizedException.java +++ b/src/main/java/io/github/nikanique/springrestframework/exceptions/UnauthorizedException.java @@ -3,8 +3,8 @@ import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; -@ResponseStatus(HttpStatus.UNAUTHORIZED) -public class UnauthorizedException extends RuntimeException { +@ResponseStatus(HttpStatus.FORBIDDEN) +public class UnauthorizedException extends BaseException { public UnauthorizedException(String message) { super(message); diff --git a/src/main/java/io/github/nikanique/springrestframework/web/responses/ErrorResponse.java b/src/main/java/io/github/nikanique/springrestframework/web/responses/ErrorResponse.java index 1402b66..ac3d321 100644 --- a/src/main/java/io/github/nikanique/springrestframework/web/responses/ErrorResponse.java +++ b/src/main/java/io/github/nikanique/springrestframework/web/responses/ErrorResponse.java @@ -1,15 +1,10 @@ package io.github.nikanique.springrestframework.web.responses; -import lombok.AllArgsConstructor; import lombok.Data; -import java.util.Map; - @Data -@AllArgsConstructor public class ErrorResponse { private String message; - private Map fields; public ErrorResponse(String message) { this.message = message; From 2cf3fad6eebfcb37dcd5d15d58b01e02ccf23924 Mon Sep 17 00:00:00 2001 From: birddevelper Date: Tue, 24 Dec 2024 22:44:29 +0200 Subject: [PATCH 2/7] fix: Handle UnauthorizedException --- .../advices/GlobalExceptionHandler.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/github/nikanique/springrestframework/advices/GlobalExceptionHandler.java b/src/main/java/io/github/nikanique/springrestframework/advices/GlobalExceptionHandler.java index d536a12..af45ac1 100644 --- a/src/main/java/io/github/nikanique/springrestframework/advices/GlobalExceptionHandler.java +++ b/src/main/java/io/github/nikanique/springrestframework/advices/GlobalExceptionHandler.java @@ -3,8 +3,10 @@ import io.github.nikanique.springrestframework.exceptions.BadRequestException; import io.github.nikanique.springrestframework.exceptions.BaseException; +import io.github.nikanique.springrestframework.exceptions.UnauthorizedException; import io.github.nikanique.springrestframework.exceptions.ValidationException; import io.github.nikanique.springrestframework.web.responses.ErrorResponse; +import io.github.nikanique.springrestframework.web.responses.ValidationErrorResponse; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ControllerAdvice; @@ -15,10 +17,15 @@ public class GlobalExceptionHandler { @ExceptionHandler({ValidationException.class, BadRequestException.class}) public ResponseEntity handleValidationException(BaseException ex) { - ErrorResponse errorResponse = new ErrorResponse(ex.getMessage(), ex.getErrors()); - return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + ValidationErrorResponse validationErrorResponse = new ValidationErrorResponse(ex.getMessage(), ex.getErrors()); + return new ResponseEntity<>(validationErrorResponse, HttpStatus.BAD_REQUEST); } + @ExceptionHandler({UnauthorizedException.class}) + public ResponseEntity handleUnauthorizedException(BaseException ex) { + ErrorResponse errorResponse = new ErrorResponse(ex.getMessage()); + return new ResponseEntity<>(errorResponse, HttpStatus.FORBIDDEN); + } } From 1209b3ae204a384a369b3055d4d5910b6fb70d58 Mon Sep 17 00:00:00 2001 From: birddevelper Date: Tue, 24 Dec 2024 22:45:47 +0200 Subject: [PATCH 3/7] fix: change the ValidationErrorResponse inheritance --- .../web/responses/ValidationErrorResponse.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/main/java/io/github/nikanique/springrestframework/web/responses/ValidationErrorResponse.java diff --git a/src/main/java/io/github/nikanique/springrestframework/web/responses/ValidationErrorResponse.java b/src/main/java/io/github/nikanique/springrestframework/web/responses/ValidationErrorResponse.java new file mode 100644 index 0000000..428c64d --- /dev/null +++ b/src/main/java/io/github/nikanique/springrestframework/web/responses/ValidationErrorResponse.java @@ -0,0 +1,15 @@ +package io.github.nikanique.springrestframework.web.responses; + +import lombok.Data; + +import java.util.Map; + +@Data +public class ValidationErrorResponse extends ErrorResponse { + private Map fields; + + public ValidationErrorResponse(String message, Map fields) { + super(message); + this.fields = fields; + } +} \ No newline at end of file From 02ca76732d57ba0c6f1af86ff8bf4d0121f522cd Mon Sep 17 00:00:00 2001 From: birddevelper Date: Tue, 24 Dec 2024 22:49:02 +0200 Subject: [PATCH 4/7] feat: Add support for Spring security Authorities and Roles --- pom.xml | 7 +++- .../controllers/BaseGenericController.java | 38 +++++++++++++++++++ .../controllers/GenericCommandController.java | 8 +++- .../controllers/GenericCreateController.java | 1 + .../controllers/GenericDeleteController.java | 10 +++++ .../controllers/GenericListController.java | 2 +- .../controllers/GenericQueryController.java | 4 +- .../GenericRetrieveController.java | 4 +- .../controllers/GenericUpdateController.java | 5 ++- 9 files changed, 69 insertions(+), 10 deletions(-) diff --git a/pom.xml b/pom.xml index 5e372cd..e258b91 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ io.github.nikanique spring-rest-framework - 1.0.2 + 2.0.0 jar spring-rest-framework @@ -72,6 +72,11 @@ spring-boot-starter-web provided + + org.springframework.boot + spring-boot-starter-security + provided + net.bytebuddy byte-buddy diff --git a/src/main/java/io/github/nikanique/springrestframework/web/controllers/BaseGenericController.java b/src/main/java/io/github/nikanique/springrestframework/web/controllers/BaseGenericController.java index a682234..c334319 100644 --- a/src/main/java/io/github/nikanique/springrestframework/web/controllers/BaseGenericController.java +++ b/src/main/java/io/github/nikanique/springrestframework/web/controllers/BaseGenericController.java @@ -1,6 +1,7 @@ package io.github.nikanique.springrestframework.web.controllers; +import io.github.nikanique.springrestframework.exceptions.UnauthorizedException; import io.github.nikanique.springrestframework.orm.SearchCriteria; import io.github.nikanique.springrestframework.serializer.Serializer; import io.swagger.v3.oas.models.Operation; @@ -11,10 +12,14 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.method.HandlerMethod; import java.lang.reflect.ParameterizedType; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * This is abstract controller. It is a base controller for all generic controllers. @@ -31,12 +36,16 @@ public abstract class BaseGenericController> endpointsRequiredAuthorities; @Autowired public BaseGenericController(@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") ModelRepository repository) { this.repository = repository; + this.endpointsRequiredAuthorities = new HashMap<>(); + this.configRequiredAuthorities(this.endpointsRequiredAuthorities); } + @Override @Autowired public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { @@ -78,4 +87,33 @@ public void setSerializer(Serializer serializer) { this.serializer = serializer; } + protected void configRequiredAuthorities(Map> authorities) { + authorities.put("GET", null); + authorities.put("POST", null); + authorities.put("PUT", null); + authorities.put("PATCH", null); + authorities.put("DELETE", null); + } + + protected List getRequiredAuthorities(String HttpMethod) { + return this.endpointsRequiredAuthorities.getOrDefault(HttpMethod, null); + } + + protected boolean hasAuthorities(List requiredAuthorities) { + if (requiredAuthorities == null) { + return true; + } + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return requiredAuthorities.stream().anyMatch(authority -> + authentication.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals(authority)) + ); + } + + protected void authorizeRequest(String HttpMethod) { + List requiredAuthorities = getRequiredAuthorities(HttpMethod); + if (!hasAuthorities(requiredAuthorities)) { + throw new UnauthorizedException("You do not have permission to perform this action."); + } + } } diff --git a/src/main/java/io/github/nikanique/springrestframework/web/controllers/GenericCommandController.java b/src/main/java/io/github/nikanique/springrestframework/web/controllers/GenericCommandController.java index 27f4d6c..e20ad7a 100644 --- a/src/main/java/io/github/nikanique/springrestframework/web/controllers/GenericCommandController.java +++ b/src/main/java/io/github/nikanique/springrestframework/web/controllers/GenericCommandController.java @@ -82,24 +82,28 @@ protected Filter configLookupFilter() { @PostMapping("/") public ResponseEntity post(HttpServletRequest request) throws IOException { + this.authorizeRequest("POST"); return this.create(this, request); } @PutMapping("/{lookup}") public ResponseEntity put(@PathVariable(name = "lookup") Object lookupValue, HttpServletRequest request) throws Throwable { + this.authorizeRequest("PUT"); return this.update(this, lookupValue, request); } @PatchMapping("/{lookup}") - public ResponseEntity partialUpdate(@PathVariable(name = "lookup") Object lookupValue, HttpServletRequest request) throws Throwable { + public ResponseEntity patch(@PathVariable(name = "lookup") Object lookupValue, HttpServletRequest request) throws Throwable { + this.authorizeRequest("PATCH"); return this.partialUpdate(this, lookupValue, request); } @DeleteMapping("/{lookup}") public ResponseEntity delete(HttpServletRequest request, @PathVariable(name = "lookup") Object lookupValue) { - return deleteObject(this, request, lookupValue); + this.authorizeRequest("DELETE"); + return this.deleteObject(this, lookupValue, request); } diff --git a/src/main/java/io/github/nikanique/springrestframework/web/controllers/GenericCreateController.java b/src/main/java/io/github/nikanique/springrestframework/web/controllers/GenericCreateController.java index 6b63c36..c30503b 100644 --- a/src/main/java/io/github/nikanique/springrestframework/web/controllers/GenericCreateController.java +++ b/src/main/java/io/github/nikanique/springrestframework/web/controllers/GenericCreateController.java @@ -74,6 +74,7 @@ public Class getCreateResponseBodyDTO() { @PostMapping("/") public ResponseEntity post(HttpServletRequest request) throws IOException { + this.authorizeRequest("POST"); return this.create(this, request); } diff --git a/src/main/java/io/github/nikanique/springrestframework/web/controllers/GenericDeleteController.java b/src/main/java/io/github/nikanique/springrestframework/web/controllers/GenericDeleteController.java index ddf6115..4afebba 100644 --- a/src/main/java/io/github/nikanique/springrestframework/web/controllers/GenericDeleteController.java +++ b/src/main/java/io/github/nikanique/springrestframework/web/controllers/GenericDeleteController.java @@ -7,9 +7,13 @@ import io.github.nikanique.springrestframework.services.QueryService; import io.swagger.v3.oas.models.Operation; import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.HttpServletRequest; import lombok.Getter; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.method.HandlerMethod; /** @@ -64,6 +68,12 @@ protected Filter configLookupFilter() { return new Filter("id", FilterOperation.EQUAL, FieldType.INTEGER); } + @DeleteMapping("/{lookup}") + public ResponseEntity delete(HttpServletRequest request, @PathVariable(name = "lookup") Object lookupValue) { + this.authorizeRequest("DELETE"); + return deleteObject(this, lookupValue, request); + } + public void customizeOperationForController(Operation operation, HandlerMethod handlerMethod) { if (handlerMethod.getMethod().getName().equals("delete")) { generateDeleteSchema(operation, getLookupFilter()); diff --git a/src/main/java/io/github/nikanique/springrestframework/web/controllers/GenericListController.java b/src/main/java/io/github/nikanique/springrestframework/web/controllers/GenericListController.java index b73ffd8..04785ba 100644 --- a/src/main/java/io/github/nikanique/springrestframework/web/controllers/GenericListController.java +++ b/src/main/java/io/github/nikanique/springrestframework/web/controllers/GenericListController.java @@ -100,7 +100,7 @@ public ResponseEntity> get( @RequestParam(defaultValue = "10") int size, @RequestParam(defaultValue = "") String sortBy, @RequestParam(defaultValue = "ASC") Sort.Direction direction) throws Throwable { - + this.authorizeRequest("GET"); return this.list(this, request, page, size, sortBy, direction); } diff --git a/src/main/java/io/github/nikanique/springrestframework/web/controllers/GenericQueryController.java b/src/main/java/io/github/nikanique/springrestframework/web/controllers/GenericQueryController.java index 027e5bb..e050ff5 100644 --- a/src/main/java/io/github/nikanique/springrestframework/web/controllers/GenericQueryController.java +++ b/src/main/java/io/github/nikanique/springrestframework/web/controllers/GenericQueryController.java @@ -120,7 +120,7 @@ public ResponseEntity> get( @RequestParam(defaultValue = "10") int size, @RequestParam(defaultValue = "") String sortBy, @RequestParam(defaultValue = "ASC") Sort.Direction direction) throws Throwable { - + this.authorizeRequest("GET"); return this.list(this, request, page, size, sortBy, direction); } @@ -128,7 +128,7 @@ public ResponseEntity> get( public ResponseEntity getByLookupValue( HttpServletRequest request, @PathVariable(name = "lookup") Object lookupValue) throws Throwable { - + this.authorizeRequest("GET"); return this.retrieve(this, request, lookupValue); } diff --git a/src/main/java/io/github/nikanique/springrestframework/web/controllers/GenericRetrieveController.java b/src/main/java/io/github/nikanique/springrestframework/web/controllers/GenericRetrieveController.java index d327c12..bf26b8d 100644 --- a/src/main/java/io/github/nikanique/springrestframework/web/controllers/GenericRetrieveController.java +++ b/src/main/java/io/github/nikanique/springrestframework/web/controllers/GenericRetrieveController.java @@ -75,7 +75,7 @@ protected String getQueryMethodName() { private void postConstruct() { this.queryService = QueryService.getInstance(this.getModel(), this.repository, this.context); } - + protected Class getRetrieveResponseDTO() { return getDTO(); @@ -93,7 +93,7 @@ protected Filter configLookupFilter() { public ResponseEntity getByLookupValue( HttpServletRequest request, @PathVariable(name = "lookup") Object lookupValue) throws Throwable { - + this.authorizeRequest("GET"); return this.retrieve(this, request, lookupValue); } diff --git a/src/main/java/io/github/nikanique/springrestframework/web/controllers/GenericUpdateController.java b/src/main/java/io/github/nikanique/springrestframework/web/controllers/GenericUpdateController.java index 8c95c79..2bf488b 100644 --- a/src/main/java/io/github/nikanique/springrestframework/web/controllers/GenericUpdateController.java +++ b/src/main/java/io/github/nikanique/springrestframework/web/controllers/GenericUpdateController.java @@ -99,13 +99,14 @@ protected Filter configLookupFilter() { @PutMapping("/{lookup}") public ResponseEntity update(@PathVariable(name = "lookup") Object lookupValue, HttpServletRequest request) throws Throwable { - + this.authorizeRequest("PUT"); return this.update(this, lookupValue, request); } @PatchMapping("/{lookup}") public ResponseEntity patch(@PathVariable(name = "lookup") Object lookupValue, HttpServletRequest request) throws Throwable { - return partialUpdate(this, lookupValue, request); + this.authorizeRequest("PATCH"); + return this.partialUpdate(this, lookupValue, request); } From c2bd6a1ed8e4ecddf92b535106d960b016004102 Mon Sep 17 00:00:00 2001 From: birddevelper Date: Tue, 24 Dec 2024 22:49:37 +0200 Subject: [PATCH 5/7] fix: Add message to list response --- .../springrestframework/web/controllers/ListController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/io/github/nikanique/springrestframework/web/controllers/ListController.java b/src/main/java/io/github/nikanique/springrestframework/web/controllers/ListController.java index 48c9208..10b6813 100644 --- a/src/main/java/io/github/nikanique/springrestframework/web/controllers/ListController.java +++ b/src/main/java/io/github/nikanique/springrestframework/web/controllers/ListController.java @@ -61,7 +61,7 @@ default ResponseEntity list(BaseGenericController controller, HttpServletRequest Page entityPage = getQueryService().getPagedlist(searchCriteriaList, page, size, direction, sortColumn, getQueryMethod()); List dtoList = entityPage.map(entity -> controller.getSerializer().serialize(entity, getListSerializerConfig())).getContent(); - PagedResponse response = new PagedResponse<>(dtoList, entityPage.getTotalElements()); + PagedResponse response = new PagedResponse<>(dtoList, entityPage.getTotalElements(), "OK"); return ResponseEntity.ok(response); } From ace61b08708c2175c72478a56999d8069f59c934 Mon Sep 17 00:00:00 2001 From: birddevelper Date: Tue, 24 Dec 2024 22:50:08 +0200 Subject: [PATCH 6/7] fix: raise exception om validation error --- .../springrestframework/web/controllers/CreateController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/io/github/nikanique/springrestframework/web/controllers/CreateController.java b/src/main/java/io/github/nikanique/springrestframework/web/controllers/CreateController.java index 5c75671..d3a01c3 100644 --- a/src/main/java/io/github/nikanique/springrestframework/web/controllers/CreateController.java +++ b/src/main/java/io/github/nikanique/springrestframework/web/controllers/CreateController.java @@ -41,7 +41,7 @@ default SerializerConfig configCreateSerializerFields() { default ResponseEntity create(BaseGenericController controller, HttpServletRequest request) throws IOException { String requestBody = this.getRequestBody(request); - Object dto = controller.getSerializer().deserialize(requestBody, getCreateRequestBodyDTO()); + Object dto = controller.getSerializer().deserialize(requestBody, getCreateRequestBodyDTO(), true); Model entity = this.getEntityHelper().fromDto(dto, this.getCreateRequestBodyDTO()); entity = getCommandService().create(entity); return ResponseEntity.status(HttpStatus.CREATED).body( From e3109fc66f3f64dd214967dfe07e5916e79ec1c5 Mon Sep 17 00:00:00 2001 From: birddevelper Date: Tue, 24 Dec 2024 22:50:41 +0200 Subject: [PATCH 7/7] refactor: change arguments order --- .../springrestframework/web/controllers/DeleteController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/io/github/nikanique/springrestframework/web/controllers/DeleteController.java b/src/main/java/io/github/nikanique/springrestframework/web/controllers/DeleteController.java index da5edca..ae0670d 100644 --- a/src/main/java/io/github/nikanique/springrestframework/web/controllers/DeleteController.java +++ b/src/main/java/io/github/nikanique/springrestframework/web/controllers/DeleteController.java @@ -26,7 +26,7 @@ public interface DeleteController { Filter getLookupFilter(); - default ResponseEntity deleteObject(BaseGenericController controller, HttpServletRequest request, Object lookupValue) { + default ResponseEntity deleteObject(BaseGenericController controller, Object lookupValue, HttpServletRequest request) { List searchCriteriaList = SearchCriteria.fromValue(lookupValue, this.getLookupFilter()); searchCriteriaList = controller.filterByRequest(request, searchCriteriaList);