spring-projects / spring-data-rest

Simplifies building hypermedia-driven REST web services on top of Spring Data repositories
https://spring.io/projects/spring-data-rest
Apache License 2.0
913 stars 561 forks source link

SDR returns 400 bad request when deserializing a linked entity where access is denied instead of a 403 forbidden #2334

Open burnumd opened 10 months ago

burnumd commented 10 months ago

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.