Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/spring security integration #57

Merged
merged 7 commits into from
Dec 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
</parent>
<groupId>io.github.nikanique</groupId>
<artifactId>spring-rest-framework</artifactId>
<version>1.0.2</version>
<version>2.0.0</version>
<packaging>jar</packaging>
<name>spring-rest-framework</name>
<description>
Expand Down Expand Up @@ -72,6 +72,11 @@
<artifactId>spring-boot-starter-web</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -15,10 +17,15 @@ public class GlobalExceptionHandler {

@ExceptionHandler({ValidationException.class, BadRequestException.class})
public ResponseEntity<ErrorResponse> 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<ErrorResponse> handleUnauthorizedException(BaseException ex) {
ErrorResponse errorResponse = new ErrorResponse(ex.getMessage());
return new ResponseEntity<>(errorResponse, HttpStatus.FORBIDDEN);
}

}

Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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.
Expand All @@ -31,12 +36,16 @@ public abstract class BaseGenericController<Model, ID, ModelRepository extends J
@Getter
protected Serializer serializer;
protected ApplicationContext context;
private Map<String, List<String>> 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 {
Expand Down Expand Up @@ -78,4 +87,33 @@ public void setSerializer(Serializer serializer) {
this.serializer = serializer;
}

protected void configRequiredAuthorities(Map<String, List<String>> authorities) {
authorities.put("GET", null);
authorities.put("POST", null);
authorities.put("PUT", null);
authorities.put("PATCH", null);
authorities.put("DELETE", null);
}

protected List<String> getRequiredAuthorities(String HttpMethod) {
return this.endpointsRequiredAuthorities.getOrDefault(HttpMethod, null);
}

protected boolean hasAuthorities(List<String> 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<String> requiredAuthorities = getRequiredAuthorities(HttpMethod);
if (!hasAuthorities(requiredAuthorities)) {
throw new UnauthorizedException("You do not have permission to perform this action.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ default SerializerConfig configCreateSerializerFields() {

default ResponseEntity<ObjectNode> 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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public interface DeleteController<Model, ID> {

Filter getLookupFilter();

default ResponseEntity<Void> deleteObject(BaseGenericController controller, HttpServletRequest request, Object lookupValue) {
default ResponseEntity<Void> deleteObject(BaseGenericController controller, Object lookupValue, HttpServletRequest request) {
List<SearchCriteria> searchCriteriaList = SearchCriteria.fromValue(lookupValue, this.getLookupFilter());
searchCriteriaList = controller.filterByRequest(request, searchCriteriaList);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,24 +82,28 @@ protected Filter configLookupFilter() {

@PostMapping("/")
public ResponseEntity<ObjectNode> post(HttpServletRequest request) throws IOException {
this.authorizeRequest("POST");
return this.create(this, request);
}

@PutMapping("/{lookup}")
public ResponseEntity<ObjectNode> put(@PathVariable(name = "lookup") Object lookupValue, HttpServletRequest request) throws Throwable {
this.authorizeRequest("PUT");
return this.update(this, lookupValue, request);
}


@PatchMapping("/{lookup}")
public ResponseEntity<ObjectNode> partialUpdate(@PathVariable(name = "lookup") Object lookupValue, HttpServletRequest request) throws Throwable {
public ResponseEntity<ObjectNode> patch(@PathVariable(name = "lookup") Object lookupValue, HttpServletRequest request) throws Throwable {
this.authorizeRequest("PATCH");
return this.partialUpdate(this, lookupValue, request);
}


@DeleteMapping("/{lookup}")
public ResponseEntity<Void> delete(HttpServletRequest request, @PathVariable(name = "lookup") Object lookupValue) {
return deleteObject(this, request, lookupValue);
this.authorizeRequest("DELETE");
return this.deleteObject(this, lookupValue, request);
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ public Class<?> getCreateResponseBodyDTO() {

@PostMapping("/")
public ResponseEntity<ObjectNode> post(HttpServletRequest request) throws IOException {
this.authorizeRequest("POST");
return this.create(this, request);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -64,6 +68,12 @@ protected Filter configLookupFilter() {
return new Filter("id", FilterOperation.EQUAL, FieldType.INTEGER);
}

@DeleteMapping("/{lookup}")
public ResponseEntity<Void> 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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public ResponseEntity<PagedResponse<ObjectNode>> 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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,15 +120,15 @@ public ResponseEntity<PagedResponse<ObjectNode>> 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);
}

@GetMapping("/{lookup}")
public ResponseEntity<ObjectNode> getByLookupValue(
HttpServletRequest request,
@PathVariable(name = "lookup") Object lookupValue) throws Throwable {

this.authorizeRequest("GET");
return this.retrieve(this, request, lookupValue);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -93,7 +93,7 @@ protected Filter configLookupFilter() {
public ResponseEntity<ObjectNode> getByLookupValue(
HttpServletRequest request,
@PathVariable(name = "lookup") Object lookupValue) throws Throwable {

this.authorizeRequest("GET");
return this.retrieve(this, request, lookupValue);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,14 @@ protected Filter configLookupFilter() {

@PutMapping("/{lookup}")
public ResponseEntity<ObjectNode> update(@PathVariable(name = "lookup") Object lookupValue, HttpServletRequest request) throws Throwable {

this.authorizeRequest("PUT");
return this.update(this, lookupValue, request);
}

@PatchMapping("/{lookup}")
public ResponseEntity<ObjectNode> patch(@PathVariable(name = "lookup") Object lookupValue, HttpServletRequest request) throws Throwable {
return partialUpdate(this, lookupValue, request);
this.authorizeRequest("PATCH");
return this.partialUpdate(this, lookupValue, request);
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ default ResponseEntity list(BaseGenericController controller, HttpServletRequest

Page<Object> entityPage = getQueryService().getPagedlist(searchCriteriaList, page, size, direction, sortColumn, getQueryMethod());
List<ObjectNode> dtoList = entityPage.map(entity -> controller.getSerializer().serialize(entity, getListSerializerConfig())).getContent();
PagedResponse<ObjectNode> response = new PagedResponse<>(dtoList, entityPage.getTotalElements());
PagedResponse<ObjectNode> response = new PagedResponse<>(dtoList, entityPage.getTotalElements(), "OK");
return ResponseEntity.ok(response);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, String> fields;

public ErrorResponse(String message) {
this.message = message;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, String> fields;

public ValidationErrorResponse(String message, Map<String, String> fields) {
super(message);
this.fields = fields;
}
}
Loading