spring-projects / spring-projects/spring-security

hasAuthority and custom Mono<Boolean> method in @PreAuthorize leads to ConverterNotFoundException error

Open
#15,209 3 comments 2 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

in: core type: enhancement
Dominant language
Java
Stars
9.6k
Forks
6.3k
Avg merge
2d 11h
Merged PRs (30d)
52

Description

Describe the bug
After upgrading to Spring Boot 3.3.0 and Spring Security 6.3.0 I've tried to migrate my single Mono<Boolean> @PreAuthorize calls to more complex ones as I thought that these tasks make it possible:

However, if we want to use the @PreAuthorize connected with a Mono<Boolean> and hasAuthority we are getting an error No converter found capable of converting from type [reactor.core.publisher.MonoFlatMap<java.lang.Boolean, ?>] to type [java.lang.Boolean]

To Reproduce

  • Create a WebFlux controller and use @EnableReactiveMethodSecurity configuration
  • Define Controller with a @PreAuthorize usage
  • Add verification returning Mono<Boolean> and call the endpoint - it works
  • Connect the custom method authorization with built-in @PreAuthorize - the exception will be thrown on call

Expected behaviour
The "complex" @PreAuthorize expression should be handled without isues if we use methods returning Mono<Boolean> and built-in hasAuthority calls.

Sample

@Configuration
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
public class SecurityConfig {
}
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping(value = "/test")
public class TestController {
    @GetMapping("/values")
    @PreAuthorize("hasAuthority('ppcv:browse') || "
                  + "(hasAuthority('ppcv:browse-scoped') && @userConnectionAuthorizerService.isUserConnected(#id))")
    public Mono<ApiResponse<List<TestObject>>> getAllValues(@RequestParam Integer id,
                                                                                       @RequestParam List<Status> statuses,
                                                                                       @RequestParam(required = false, defaultValue = "false") Boolean latest) {
        return ...
    }
}
public interface UserConnectionAuthorizerService {

    Mono<Boolean> isUserConnected(Long id);

}

When running code in the integration tests I receive:

2024-06-06 18:55:42,135 [parallel-4] ERROR o.s.b.a.w.r.e.AbstractErrorWebExceptionHandler - [5f1ceeb9-3]  500 Server Error for HTTP GET "/test/values?id=1&statuses=APPROVED"
java.lang.IllegalArgumentException: Failed to evaluate expression 'hasAuthority('ppcv:browse') || (hasAuthority('ppcv:browse-scoped') && @userConnectionAuthorizerService.isUserConnected(#id))'
	at org.springframework.security.authorization.method.ReactiveExpressionUtils.lambda$evaluate$0(ReactiveExpressionUtils.java:43)
	Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
	*__checkpoint ⇢ Handler test.TestController#getAllValues(Integer, List, Boolean) [DispatcherHandler]
	*__checkpoint ⇢ test.core.infrastructure.actuator.spring.DefaultActuatorWebFilter [DefaultWebFilterChain]
	*__checkpoint ⇢ test.core.application.spring.http.filter.AdminSecurityFilter [DefaultWebFilterChain]
	*__checkpoint ⇢ AuthorizationWebFilter [DefaultWebFilterChain]
	*__checkpoint ⇢ ExceptionTranslationWebFilter [DefaultWebFilterChain]
	*__checkpoint ⇢ LogoutWebFilter [DefaultWebFilterChain]
	*__checkpoint ⇢ ServerRequestCacheWebFilter [DefaultWebFilterChain]
	*__checkpoint ⇢ SecurityContextServerWebExchangeWebFilter [DefaultWebFilterChain]
	*__checkpoint ⇢ UserIdAuthenticationFilter [DefaultWebFilterChain]
	*__checkpoint ⇢ ReactorContextWebFilter [DefaultWebFilterChain]
	*__checkpoint ⇢ HttpHeaderWriterWebFilter [DefaultWebFilterChain]
	*__checkpoint ⇢ ServerWebExchangeReactorContextWebFilter [DefaultWebFilterChain]
	*__checkpoint ⇢ org.springframework.security.web.server.WebFilterChainProxy [DefaultWebFilterChain]
	*__checkpoint ⇢ HTTP GET "/test/values?id=1&statuses=APPROVED" [ExceptionHandlingWebHandler]
Original Stack Trace:
		at org.springframework.security.authorization.method.ReactiveExpressionUtils.lambda$evaluate$0(ReactiveExpressionUtils.java:43)
		...
Caused by: org.springframework.expression.spel.SpelEvaluationException: EL1001E: Type conversion problem, cannot convert from reactor.core.publisher.MonoJust<java.lang.Boolean> to java.lang.Boolean
	at org.springframework.expression.spel.support.StandardTypeConverter.convertValue(StandardTypeConverter.java:87)
	at org.springframework.expression.common.ExpressionUtils.convertTypedValue(ExpressionUtils.java:57)
	at org.springframework.expression.spel.ast.SpelNodeImpl.getValue(SpelNodeImpl.java:222)
	at org.springframework.expression.spel.ast.OpAnd.getBooleanValue(OpAnd.java:57)
	at org.springframework.expression.spel.ast.OpAnd.getValueInternal(OpAnd.java:52)
	at org.springframework.expression.spel.ast.SpelNodeImpl.getValue(SpelNodeImpl.java:222)
	at org.springframework.expression.spel.ast.OpOr.getBooleanValue(OpOr.java:56)
	at org.springframework.expression.spel.ast.OpOr.getValueInternal(OpOr.java:51)
	at org.springframework.expression.spel.ast.OpOr.getValueInternal(OpOr.java:37)
	at org.springframework.expression.spel.ast.SpelNodeImpl.getValue(SpelNodeImpl.java:114)
	at org.springframework.expression.spel.standard.SpelExpression.getValue(SpelExpression.java:273)
	at org.springframework.security.authorization.method.ReactiveExpressionUtils.lambda$evaluate$2(ReactiveExpressionUtils.java:39)
	...
Caused by: org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [reactor.core.publisher.MonoJust<java.lang.Boolean>] to type [java.lang.Boolean]
	at org.springframework.core.convert.support.GenericConversionService.handleConverterNotFound(GenericConversionService.java:294)
	at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:185)
	at org.springframework.expression.spel.support.StandardTypeConverter.convertValue(StandardTypeConverter.java:82)
	... 260 common frames omitted

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in ReactiveExpressionUtils.java, where the stack trace shows reactive method-security expressions are evaluated, and trace how a compound expression combines hasAuthority with a Mono result. Reproduce the WebFlux case with @EnableReactiveMethodSecurity and the provided @PreAuthorize expression; done means the compound expression evaluates without a ConverterNotFoundException.

Written by the indexing model from the issue text.

Assessment

Tech stack
java, spring
Domain
backend, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.