I have this RestController where I use @PreAuthorize to check if the request contains the same 'persnr' as the user.
@PreAuthorize("@authorization.hasPermission(principal, #request)")
@PostMapping("/items")
public ResponseEntity<CalendarResponse> getCalendarItems(@Valid @NotNull @RequestBody CalendarRequest request)
{
...
}
This is the @Bean with the actual 'hasPermission' method the spEL in @PreAutorize refers to
@Component
public class Authorization
{
public boolean hasPermission(User user, /* null in native image */ CalendarRequest request)
{
return user.getPersnr().equals(request.getPersnr());
}
}
This is the body of the request
public class CalendarRequest
{
@NotBlank(message = "\"persnr darf nicht leer sein")
private String persnr;
public String getPersnr()
{
return persnr;
}
// ...
}
In a JVM this works perfectly fine but as a native image the '#request' in the spEL is null and i get a NullPointerException in 'hasPermission'.
As a workaround I tried to register a WebExpressionAuthorizationManager with the same spEL but it's not working either
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception
{
http.authorizeHttpRequests(auth ->
auth
//...
.dispatcherTypeMatchers(DispatcherType.ERROR).permitAll()
.requestMatchers("/calendar/items").access(new WebExpressionAuthorizationManager("@authorization.hasPermission(principal, #request)"))
.anyRequest().authenticated());
//...
return http.build();
}
I also tried using a custom AuthorizationManager but i don't see any possibility to access the json body
Is there another way to implement method level authorization?
Edit: I set'prePostEnabled' to false and implemented a custom AuthorizationManager with the same behavior like @PreAuthorize but the '#request' parameter is still null:
@Configuration
@EnableMethodSecurity(prePostEnabled = false, securedEnabled = false, jsr250Enabled = false)
public class AuthorizationConfig
{
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor preAuthorize(CustomAuthorizationManager manager)
{
Pointcut pointcut = new AnnotationMatchingPointcut(null, PreAuthorize.class, true);
AuthorizationManagerBeforeMethodInterceptor interceptor = new AuthorizationManagerBeforeMethodInterceptor(pointcut, manager);
interceptor.setOrder(AuthorizationInterceptorsOrder.PRE_AUTHORIZE.getOrder());
return interceptor;
}
}
@Component
public class CustomAuthorizationManager implements AuthorizationManager<MethodInvocation>
{
@Autowired
ApplicationContext context;
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation mi)
{
PreAuthorize annotation = mi.getMethod().getAnnotation(PreAuthorize.class);
DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
expressionHandler.setApplicationContext(context);
Expression expression = expressionHandler.getExpressionParser().parseExpression(annotation.value());
EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication, mi);
boolean granted = ExpressionUtils.evaluateAsBoolean(expression, ctx);
return new ExpressionAuthorizationDecision(granted, expression);
}
}
In the depth of spring I found out that the parameter name of the method which was retrieved by reflection was 'arg0', so it couldn't be mapped to the '#request' literal in the spEL expression. I compiled with the compiler arg -parameter which preserves the parameter names and now everything works fine.
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.3</version>
<configuration>
<source>${java-version}</source>
<target>${java-version}</target>
<compilerArgs>
<arg>-parameters</arg> <---- this did the magic
</compilerArgs>
</configuration>
</plugin>