Expected Behavior When we configure MessageMatcherDelegatingAuthorizationManager it should be possible to provide an instance of a class that supports expression-based authorization to the method org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager.Builder.Constraint.access(AuthorizationManager<MessageAuthorizationContext<?>>)

In non-websocket world this is supported by org.springframework.security.web.access.expression.WebExpressionAuthorizationManager.WebExpressionAuthorizationManager(String)

Current Behavior WebSocket class that seems to be facilitating the same task (org.springframework.security.config.websocket.WebSocketMessageBrokerSecurityBeanDefinitionParser.ExpressionBasedAuthorizationManager) is private and looks to be used for XML config only

Context Spring Security 5.8.1. The issue is affecting us during migration from older Spring Security which supported expression-based access control via the method org.springframework.security.config.annotation.web.messaging.MessageSecurityMetadataSourceRegistry.Constraint.access(String)

Comment From: i-filaliawb

same problem with us.

can not find any way to use expression-based access control for destination.

Comment From: reckart

Has anybody figured out how to do expression-based authentication with WebSocket in Spring 6?

Comment From: jzheaux

Hi, @lrozenblyum, thanks for reaching out. The main reason this does not exist in the MessageMatcherDelegatingAuthorizationManager is that we'd like to encourage people to move away from SpEL in their web authorization rules and use a concrete implementation of AuthorizationManager instead. The primary benefit is that it is independently testable.

That said, I think there's value in adding a messaging equivalent to WebExpressionAuthorizationManager to simplify migration.

Can someone submit a PR?

In the meantime, you are welcome to use the following implementation:

public final class MessageExpressionAuthorizationManager implements AuthorizationManager<MessageAuthorizationContext<?>> {

    private SecurityExpressionHandler<Message<?>> expressionHandler = new DefaultMessageSecurityExpressionHandler();

    private Expression expression;

    public MessageExpressionAuthorizationManager(String expressionString) {
        Assert.hasText(expressionString, "expressionString cannot be empty");
        this.expression = this.expressionHandler.getExpressionParser().parseExpression(expressionString);
    }

    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, MessageAuthorizationContext<?> context) {
        EvaluationContext ctx = this.expressionHandler.createEvaluationContext(authentication, context.getMessage());
        boolean granted = ExpressionUtils.evaluateAsBoolean(this.expression, ctx);
        return new ExpressionAuthorizationDecision(granted, this.expression);
    }

}

Then, you would use it in your authorization rules like so:

.simpDestMatchers(...).access(new MessageExpressionAuthorizationManager("my expression"))

Comment From: reckart

This eventually worked for me:

Source: https://github.com/inception-project/inception/blob/eb6262d6020e914831e677d971aa1cb4641a0d8a/inception/inception-support/src/main/java/de/tudarmstadt/ukp/inception/support/http/ServerTimingWatch.java

import java.util.function.Supplier;

import org.springframework.expression.Expression;
import org.springframework.security.access.expression.ExpressionUtils;
import org.springframework.security.access.expression.SecurityExpressionHandler;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.messaging.access.intercept.MessageAuthorizationContext;

public class MessageExpressionAuthorizationManager
    implements AuthorizationManager<MessageAuthorizationContext<?>>
{
    private final SecurityExpressionHandler<MessageAuthorizationContext<?>> expressionHandler;

    private final Expression expression;

    public static MessageExpressionAuthorizationManager expression(
            SecurityExpressionHandler<MessageAuthorizationContext<?>> expressionHandler,
            String aExpression)
    {
        return new MessageExpressionAuthorizationManager(expressionHandler, aExpression);
    }

    private MessageExpressionAuthorizationManager(
            SecurityExpressionHandler<MessageAuthorizationContext<?>> aExpressionHandler,
            String aExpression)
    {
        expressionHandler = aExpressionHandler;
        expression = this.expressionHandler.getExpressionParser().parseExpression(aExpression);
    }

    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication,
            MessageAuthorizationContext<?> object)
    {
        var context = expressionHandler.createEvaluationContext(authentication, object);
        var granted = ExpressionUtils.evaluateAsBoolean(expression, context);
        return new AuthorizationDecision(granted);
    }
}

Source: https://github.com/inception-project/inception/blob/eb6262d6020e914831e677d971aa1cb4641a0d8a/inception/inception-websocket/src/main/java/de/tudarmstadt/ukp/inception/websocket/config/WebsocketSecurityConfig.java

        var msgSecurityExpressionHandler = new DefaultMessageSecurityExpressionHandler();
        msgSecurityExpressionHandler.setApplicationContext(aContext);
        msgSecurityExpressionHandler.setPermissionEvaluator(aEval);
        var mah = new MessageAuthorizationContextSecurityExpressionHandler(
                msgSecurityExpressionHandler);

        messages //
            ...
            .simpSubscribeDestMatchers("/*/scheduler" + TOPIC_ELEMENT_PROJECT + "{" + PARAM_PROJECT + "}")
                .access(expression(mah, "@projectAccess.canManageProject(#" + PARAM_PROJECT + ")"))

Note: in the latest Spring versions, the check method of the authorization manager has been deprecated and the verify method should be implemented instead.

Comment From: reckart

The main reason this does not exist in the MessageMatcherDelegatingAuthorizationManager is that we'd like to encourage people to move away from SpEL in their web authorization rules and use a concrete implementation of AuthorizationManager instead. The primary benefit is that it is independently testable.

@jzheaux is there a migration example illustrating the transition from the old expression-based approach to this new imagined approach available somewhere?

Comment From: jzheaux

I think that would be nice to add, @reckart. Do you have an example from your code base you wouldn't mind sharing? Then we could accomplish two things at once by adding that or a variation of it into the documentation.

Comment From: jzheaux

Also, it appears you've already done a lot of the work to create an authorization manager. Are you able to contribute it in a PR?

Comment From: reckart

@jzheaux if your idea is to migrate away from the expressions and instead move to people using bespoke AuthorizationManagers for all kinds of stuff, having this in as a PR seems like a step backwards.

In my codebase, I have some XXXAccess beans where the access control is centralized. Currently, those are accessed via the expressions because that is the way I found out how to do it. Until you brought it up, I didn't think about it in the way that I might kind of promote/wrap that access code into a bespoke AuthorizationManager. It would certainly give the compiler the option of checking more of the consistency and make the whole setup less fragile.

So I think giving people an example of how to move from the expression-based implementation to a code-based implementation would be more valuable.

Comment From: reckart

I have added source links to the code snippets.

Comment From: reckart

@jzheaux you might want to update the documentation PR to implement the verify method instead of check since the latter has been deprecated.

Comment From: reckart

Hm... I wonder if a custom AuthorizationManager that does not use SpEL would be able to show as nicely the connection between the topic variables and the access control. Also, using the SpEL approach, I only have to implement one access check that works for http access, messaging and can even be used programmatically from elsewhere in the code.

 .simpSubscribeDestMatchers("/*/scheduler" + TOPIC_ELEMENT_PROJECT + "{" + PARAM_PROJECT + "}")
                .access(expression(mah, "@projectAccess.canManageProject(#" + PARAM_PROJECT + ")"))

If I have to implement special AuthorizationManagers wrapping these calls for http and messaging, it adds extra code and I doubt the coherence would be similarly clear in the code.

Comment From: mehrdadbozorgmehr

Can I work on this issue? Please assign it to me.