Spring Security provides expressive ways to declare authorization. For example HttpSecurity provides:

http
    .authorizeHttpRequests((r) -> r
        .requestMatchers("/admin/**").hasRole("ADMIN")
    )

Right now this always maps to AuthorityAuthorizationManager.hasRole(String). However, there may be times users want it to map to a custom AuthorizationManager. For example, they might want to map it to something like OPA, OpenFG, or some other external service.

These customizations can currently be done using something like, but it looses some of its expressiveness and makes it more difficult to swap out the implementation:

http
    .authorizeHttpRequests((r) -> r
        .requestMatchers("/admin/**").access(new CustomAuthorizationManager())
    )

For this ticket we should:

1) We should create an API that abstracts how the expressions are created and delegates to the existing APIs. For example:

public interface AuthorizationManagerFactory<T> {
    AuthorizationManager<T> permitAll();
    AuthorizationManager<T> denyAll();
    AuthorizationManager<T> hasRole(String role);
    AuthorizationManager<T> hasAnyRole(String... roles);
    AuthorizationManager<T> hasAuthority(String authority);
    AuthorizationManager<T> hasAnyAuthority(String... authorities);
....
}

2) Create a default implementation that delegates to the existing static methods that are used now.

3) Update Then the HttpSecurity DSL would use the default implementation that delegates to the existing static methods. If an AuthorizationManagerFactory Bean is published, it would be used by the DSL. This should be generic aware to ensure that it doesn't pick a factory that creates the wrong AuthorizationManager types.

4) Similar to HttpSecurity, update Method Security use a default AuthorizationManagerFactory but if a Bean is published, then use it. This should be generic aware to ensure that it doesn't pick a factory that creates the wrong AuthorizationManager types.

cc @sjohnr

Comment From: sjohnr

Hey @rwinch, I'd like to tackle this one if I may.

Comment From: rwinch

Thanks @sjohnr! The issue is all yours!

Comment From: sjohnr

Hi @rwinch. I have a quick question. You can check out this branch for context.

Similar to HttpSecurity, update Method Security use a default AuthorizationManagerFactory but if a Bean is published, then use it.

I'm wondering if you have feedback on where we might pick up and use the AuthorizationManagerFactory in method security?

For example, would it be used to create an authorization manager in SecuredMethodSecurityConfiguration? I don't imagine that users would often want to replace SecuredAuthorizationManager, but they might want to replace the AuthoritiesAuthorizationManager it uses here (which is an AuthorizationManager<Collection<String>>).

I have sketched out one idea of how this might work in this commit, but I'm not sure if this ideal or what we want to do. Since users can easily customize the MethodInterceptor by publishing their own bean, I wonder if using the factory here is really required?

Any other ideas for where the factory could be used? Or perhaps we just start with steps 1-3 and wait to see if step 4 is truly needed?

Comment From: rwinch

Thanks for working on this @sjohnr!

I didn't get a chance to look at your branch yet, but hopefully this will get you moving forward.

I don't think that you need to do anything to SecuredAuthorizationManager because that can be solved by injecting the AuthorizationManager as you described.

I was imagining that method security would customize the built in expressions like @PreAuthorize("hasRole('ADMIN')") that align with the AuthorizationManagerFactory method.

The steps I'd take are:

1) Add the AuthorizationManagerFactory to the MethodSecurityExpressionRoot (probably the base class) and update the methods like hasRole(String) to delegate to AuthorizationManagerFactory.hasRole(String).authorize(authentication, method).

2) Update createSecurityExpressionRoot method to set any AuthorizationManagerFactory<Method> on the expression root using the setter added in step 1.

Let me know if this helps or not. If not, I can try and take a deeper look.

Comment From: sjohnr

Thanks @rwinch. I think we can definitely explore that approach.

Add the AuthorizationManagerFactory to the MethodSecurityExpressionRoot (probably the base class)

The only issue I see here is that modifying the base class SecurityExpressionRoot (which is public) may have a larger impact than we want. It would need to become a generic class so it can take a generic AuthorizationManagerFactory<T>. It has multiple subclasses and potential uses in the wild which would need to change. Ideally, I think uses outside the framework shouldn't have to change.

Thinking about this, perhaps we could simply refactor MethodSecurityExpressionRoot instead (which is not public) to no longer extend SecurityExpressionRoot?

Update: This commit shows what this might look like.

Comment From: sjohnr

@rwinch I've pushed an update to the branch. It's rough at the moment, but shows a generic SecurityExpressionRoot<T> that uses an AuthorizationManagerFactory<T>.

There are some impacts to subclasses, and to AbstractSecurityExpressionHandler subclasses. I've made attempts to maintain backwards compatibility, but you might notice it gets messy. I'll wait for review on this iteration before proceeding any further.

Related issue I opened: gh-17667

Comment From: sjohnr

I've opened draft PR #17673 to review these changes.

Comment From: rwinch

Closed in favor of the PR https://github.com/spring-projects/spring-security/pull/17673