spring-projects / spring-projects/spring-data-rest
SDR returns 400 bad request when deserializing a linked entity where access is denied instead of a 403 forbidden
@odrotbohm is already working on this.
Since Nov 20, 2023.
- Dominant language
- Java
- Stars
- 958
- Forks
- 568
- PR merge metrics
- No merged PRs in 30d
Description
When creating an entity that contains a linked resource whose repository's find one method is restricted, SDR throws an HttpMessageNotReadable exception during deserialization of the linked resource (so it can't be changed using @ControllerAdvice exception handlers, resulting in the end-user receiving a 400 bad request response on a well-formed and valid request instead of the expected 403 forbidden status one would receive if they failed the global method security on any other kind of request. The following example code demonstrates the behavior by performing a POST to /parents with the body { "id":"1" } and then a POST to /children with the body { "id": "child1", "parent": "/parents/1" }.
Security Configuration: in-memory user user with password password and basic auth configured
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
@Bean
public UserDetailsService users() {
UserDetails user = User.builder()
.username("user")
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated());
http.httpBasic(withDefaults());
return http.build();
}
}
Parent domain object
@Entity
@Data
@Table(name = "parent_t")
public class Parent {
@Id
private String id;
@OneToMany
private List<Child> children;
}
Parent repository with method security
public interface ParentRepository extends CrudRepository<Parent, String> {
@Override
@PreAuthorize("isAuthenticated()")
@PostAuthorize("@authoritiesUtil.canViewParent(returnObject)")
Optional<Parent> findById(String id);
@Override
@PreAuthorize("isAuthenticated()")
<S extends Parent> S save(S entity);
}
Child domain object
@Entity
@Data
@Table(name = "child_t")
public class Child {
@Id
private String id;
@ManyToOne(optional = false)
private Parent parent;
}
Child repository with method security:
public interface ChildRepository extends CrudRepository<Child, String> {
@Override
@PreAuthorize("isAuthenticated()")
<S extends Child> S save(S entity);
}
Authorities utility class referenced in parent repository:
@Component
public class AuthoritiesUtil {
public boolean canViewParent(Optional<Parent> parent) {
return false; //insert logic to allow this in some cases
}
}
Application class
@SpringBootApplication
@EnableMethodSecurity
public class SpringDataDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringDataDemoApplication.class, args);
}
}
POM
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.5</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>edu.iu.es.ep.demo</groupId>
<artifactId>spring-data-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-data-demo</name>
<description>spring-data-demo</description>
<dependencies>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-rest-hal-explorer</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<builder>paketobuildpacks/builder-jammy-base:latest</builder>
</image>
</configuration>
</plugin>
</plugins>
</build>
</project>
In this case, I would expect to receive a 403 forbidden status instead of a 400 bad request with the message JSON parse error: Access Denied because the request is well-formatted and the linked object exists, but the user is forbidden from using it. Please let me know if you have any questions or concerns. Thank you.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.