Skip to content

Commit

Permalink
Merge pull request #59 from nikanique/develop
Browse files Browse the repository at this point in the history
Spring security
  • Loading branch information
birddevelper authored Dec 25, 2024
2 parents b2291c1 + 7f053de commit 9822d06
Show file tree
Hide file tree
Showing 18 changed files with 154 additions and 60 deletions.
55 changes: 22 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,20 @@ framework.

## Features

- **Developer-Friendly**: Effortlessly generates web APIs while eliminating the need for
boilerplate code, simplifying the task of exposing standard endpoints (GET, POST, PUT, PATCH) for ORM entity.
- **Serialization**: The comprehensive and flexible serializer, integrated with Spring Data JPA, assists developers in
customizing the input and output in web APIs.
- **Filters**: Offers flexibility and powerful filtering in APIs to query data from database.
- **Developer-Friendly**: Simplifies API development by automatically generating web endpoints (GET, POST, PUT, PATCH)
for ORM entities, eliminating boilerplate code and streamlining common tasks.
- **Serialization**: Robust and flexible serializer, seamlessly integrated with Spring Data JPA, enabling
easy customization of API input and output formats.
- **Filtering**: Provides powerful and versatile filtering capabilities for querying data directly from the database.
- **Validation**: Ensures reliable endpoint validation with a wide range of rules and supports custom validation for
user input.
- **Security**: Built-in integration with Spring Security delivers secure access control, safeguarding APIs and
resources for authorized users.

## Requirements

- Java 17+
- Spring Boot 3.0.0+
- Spring Data JPA
- Spring Web MVC
- SpringDoc OpenAPI WebMVC UI 2.1.0+

## Installation

Expand All @@ -29,23 +30,11 @@ To install the Spring REST Framework, include the following dependencies in your
```xml

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.1.0</version>
</dependency>
<!-- Other dependencies -->
<dependency>
<groupId>io.github.nikanique</groupId>
<artifactId>spring-rest-framework</artifactId>
<version>1.0.2</version>
<version>2.1.0</version>
</dependency>
</dependencies>
```
Expand All @@ -64,7 +53,6 @@ To start using the library, follow these steps:
For example, declare a Student model.

```java

import jakarta.persistence.Entity;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
Expand All @@ -73,20 +61,21 @@ To start using the library, follow these steps:
@Entity
@Data
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String fullName;
private Integer age;
private String major;

@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "school_id")
private School school;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String fullName;
private Integer age;
private String major;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "school_id")
private School school;

}

```

Create Repository for you model.
```java
import com.example.demo.model.Student;
Expand Down
15 changes: 9 additions & 6 deletions 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.1.0</version>
<packaging>jar</packaging>
<name>spring-rest-framework</name>
<description>
Expand Down Expand Up @@ -43,13 +43,11 @@
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
Expand All @@ -59,18 +57,23 @@
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<scope>provided</scope>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
<scope>provided</scope>
<version>1.18.34</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>provided</scope>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
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
@@ -0,0 +1,31 @@
package io.github.nikanique.springrestframework.configs.security;


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
@Order(Ordered.HIGHEST_PRECEDENCE)
class SpringRestFrameworkSecurityConfig {

private static void disableAuthentication(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry authz) {
try {
authz.anyRequest().permitAll().and().csrf().disable();
} catch (Exception e) {
throw new RuntimeException(e);
}
}

@Bean
public SecurityFilterChain SRFSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(SpringRestFrameworkSecurityConfig::disableAuthentication);
return http.build();
}
}
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
Loading

0 comments on commit 9822d06

Please sign in to comment.