codependent
codependent

Reputation: 24472

Secure a Spring Webflux controller considering dynamic parameters with Spring Security annotations

I have an application with some controllers that require access control based on the requested resources ids, checking against the Spring Security user Authentication roles. At the moment I have created a function that checks this condition returning a Mono<True> if it is ok (so that I can flatmap it) or an empty Mono (and also setting a 403 status code) otherwise:

@RestController
@RequestMapping("/api/v1/clients/{clientId}/departments/{departmentId}/users")
class UserRestController(private val userService: UserService) {

    @GetMapping
    fun getAll(principal: Principal, response: ServerHttpResponse,
               @PathVariable clientId: String, @PathVariable departmentId: String): Flux<Users> {
        return checkDepartmentViewPermissions(principal, response, clientId, departmentId)
                .flatMap {
                    userService.getAll(clientId, departmentId)
                }
    }

    ...
}

fun checkDepartmentViewPermissions(principal: Principal, response: ServerHttpResponse, 
         clientId: String, departmentId: String): Mono<Boolean> {
    val authentication = principal as MyAuthentication
    authentication.authorities.contains(SimpleGrantedAuthority("${clientId}:${departmentId}")).toMono()
            .filter {
                it == true
            }.switchIfEmpty {
                response.statusCode = HttpStatus.FORBIDDEN
                Mono.empty()
            }
}


As seen above, requests are in the format /api/v1/clients/{clientId}/departments/{departmentId}/users where clientId and departmentId are dynamic path variables.

The checkDepartmentViewPermission method accesses the Authentication roles where users will have a list such as (client1:department1, client1:department2, client2:department1). Thus, a URL /api/v1/clients/client1/departments/department1/users would work fine for those permissions.

Although what I have works, I would like to use a more declarative way to deal with this if it is possible, ideally something based on Spring Security annotations and having into account I need to access PathVariable parameters, something like (I'm making it up):

@RestController
@RequestMapping("/api/v1/clients/{clientId}/departments/{departmentId}/users")
class UserRestController(private val userService: UserService) {

    @PreAuthorize("#{principal.roles.contains(clientId:departmentId)}")
    @GetMapping
    fun getAll(principal: Principal, response: ServerHttpResponse,
               @PathVariable clientId: String, @PathVariable departmentId: String): Flux<Users> {
        return userService.getAll(clientId, departmentId)
    }

    ...
}

Does Spring Security support a way to do this? If not, could you suggest any ideas to achieve it?

Upvotes: 0

Views: 804

Answers (1)

codependent
codependent

Reputation: 24472

First off, thanks Thomas for pointing me in the right direction, I hadn't realized we can invoke a Spring bean from the security web expressions. This way, there's no need anymore to inject the principal since the Authentication object will be passed to the bean.

Controller:

   @PreAuthorize("@permissionChecker.hasDepartmentViewPermissions(authentication, #clientId, #departmentId)")
    @GetMapping
    fun getAll(@PathVariable clientId: String, @PathVariable departmentId: String): Flux<Users> {
        return userService.getAll(clientId, departmentId)
    }

PermissionChecker bean:

class PermissionChecker {

    fun hasDepartmentViewPermissions(authentication: Authentication, clientId: String, projectId: String): Boolean {
        ...
    }

Upvotes: 1

Related Questions