crizzis
crizzis

Reputation: 10716

Generic HTTP security path matcher, reference matched path in security expression

Right now, with Spring Security's HttpSecurity, we're able to restrict wildcard paths to specific roles/authorities:

.mvcMatchers(POST, "/users").hasAuthority("create:users")
.mvcMatchers(PUT, "/users/{id}").hasAuthority("update:users")

is there an easy way to do:

.mvcMatchers(POST, "/{whateverGoesHere}").hasAuthority("create:${whateverGoesHere}")
.mvcMatchers(PUT, "/{whateverGoesHere}/{id}").hasAuthority("update:${whateverGoesHere}")

?

It doesn't have to be a solution using the configure(HttpSecurity http) API specifically, I'm just looking for an easy way to generify authorization rules for multiple REST entities at once.

Upvotes: 1

Views: 1001

Answers (2)

Steve Riesenberg
Steve Riesenberg

Reputation: 6158

This is obviously a more advanced scenario, to say the least. However, improvements in Spring Security 5.5 have introduced the new AuthorizationManager interface and the http.authorizeHttpRequests() method for configuring authorization rules that utilize it. See The AuthorizationManager in the reference docs for more info. It is extremely powerful! I believe this is probably the best option for your use case.

There are numerous implementations available in Spring Security that can be used to build composite and/or delegating implementations. Here's an example that uses your convention:


public final class ResourceAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {

    private final String action;

    public ResourceAuthorizationManager(String action) {
        this.action = action;
    }

    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext context) {
        AuthorizationManager<RequestAuthorizationContext> delegate =
            AuthorityAuthorizationManager.hasAuthority(createAuthority(context));
        return delegate.check(authentication, context);
    }

    private String createAuthority(RequestAuthorizationContext context) {
        String resource = context.getVariables().get("resource");
        return String.format("%s:%s", this.action, resource);
    }

}

The action can be create, read, update, delete or anything you like as part of your authority string. This implementation relies on URI variables provided through the RequestAuthorizationContext. As it happens, there's an existing implementation (RequestMatcherDelegatingAuthorizationManager) that handles that scenario. It is actually the one handling .mvcMatchers() authorization rules in the Spring Security DSL. Here's an example that uses it to delegate to the convention-based AuthorizationManager above:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests
            .mvcMatchers(HttpMethod.POST, "/{resource}").access(new ResourceAuthorizationManager("create"))
            .mvcMatchers(HttpMethod.PUT, "/{resource}/{id}").access(new ResourceAuthorizationManager("update"))
            .anyRequest().authenticated()
        )
        .formLogin(Customizer.withDefaults());
    return http.build();
}

Upvotes: 3

3Fish
3Fish

Reputation: 688

I think that you should not have hidden behavior in your code. If a developer wants to add a new endpoint and wants to have it require some authority, it should be done intentionally. Otherwise, it could become a debugging nightmare if the dev intends to add an open endpoint and wonders why it is secured.

But you could add a default behavior for all endpoints that you did not specify. That behavior could be to deny access. That way, every developer has to add some kind of access granting entry. That would guarantee that it is not forgotten, but it is still intentionally done.

...
.mvcMatchers(POST, "/users").hasAuthority("create:users")
.mvcMatchers(PUT, "/users/{id}").hasAuthority("update:users")
.anyRequest().denyAll()

Upvotes: 0

Related Questions