It would be nice if Spring Security's method security meta-annotation support allowed for parameters.

For example, it would be nice to be able to do:

@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAuthority('{value}')")
public @interface HasAuthority {
    String value();
}

Then, an application could do:

@HasAuthority("message:read")
public String method(...) {
}

The annotation expression should be able to handle method parameters, like @PreAuthorize already does:

@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("@myAuthzBean.authorizeParameter(#object)")
public @interface AuthorizeObject {
}

allowing an application to do:

@AuthorizeObject
public String method(Object object) {
}

Also, it should support passing method parameters through the custom annotation where:

@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("@openfga.check({user}, {relation}, {_object})")
public @interface Check {
    String user() default "authentication.name";
    String relation();
    String _object();
}

supports:

@GetMapping("/resource/{id}")
@Check(relation="'reader'", _object="'#id'")
public String method(String id) {
}

Comment From: kse-music

@jzheaux Can I do it?

Comment From: jzheaux

Hi, @kse-music, thanks for the offer. I'm going to take care of this one for the moment since it is in a bit of flux (notice the recent changes in the description).

Please feel free to try out this branch to suggest improvements: https://github.com/jzheaux/spring-security/tree/gh-14480

Comment From: kse-music

Oh, according to the previous description, I thought that the meta-annotation parameter was used as an SPEL expression variable, but I didn't expect it used as a placeholder parsed property sources. @jzheaux

Comment From: jzheaux

That's right, @kse-music. As I was doing some of my own research, I found that approach to be easier. The reason is that SpEL does not support nested variables, which would be needed in some setups.

Sorry for the confusion. If you are still interested, I'd love to have your help on this PR: https://github.com/spring-projects/spring-security/pull/14494 once I have it out of draft.

Comment From: sbrannen

Very interesting idea! 👍

The reason is that SpEL does not support nested variables, which would be needed in some setups.

Would you mind providing a concrete example just for the sake of clarity?

Comment From: jzheaux

Sure, @sbrannen. In the following arrangement:

@PreAuthorize("hasAuthority(#authority)")
public @interface HasAuthority {
    String authority();
}

// ...

@HasAuthority("#parameter")
public String method(String parameter) { // an existing Spring Security annotation feature 
   // ...
}

what happens is if method("value") is invoked, the set of variables handed to the evaluation context looks something like:

{ "#parameter" : value, "#authority" : "#parameter" }

I couldn't quite unravel how to get SpEL to resolve #authority by first resolving #parameter.

Comment From: efenderbosch-atg

Will this enhancement let me do something like this?

@PreAuthorize(
    """
    hasAuthority('PERM_FOO') and
    (#checkTenantId == false or #tenantId == authentication.principal.tenantId)
    """
)
annotation class HasFooPermission(val checkTenantId: Boolean = true)
@RestController
@SecurityRequirement(name = "Bearer Authentication")
@RequestMapping(value = "/foo", produces = [MediaType.APPLICATION_JSON_VALUE])
class FooController(private val service: FooService) {
    @HasFooPermission(checkTenantId = false)
    @GetMapping
    fun findAll(): ResponseEntity<List<Foo>> = service.findAll().ok()

    @HasFooPermission
    @GetMapping("{tenantId}")
    fun findAllByTenantId(tenantId: UUID): ResponseEntity<List<Foo>> = service.findAllByTenantId(tenantId).ok()
}

Comment From: kse-music

@efenderbosch-atg According to the current implementation, attribute method variables in custom annotations need to be quoted through curly braces like so:

@PreAuthorize(
    """
    hasAuthority('PERM_FOO') and
    ({checkTenantId} == false or #tenantId == authentication.principal.tenantId)
    """
)
annotation class HasFooPermission(val checkTenantId: Boolean = true)