24. State Machine Security

Security features are built atop of functionality from a Spring Security. Security features are handy when it is required to protect part of a state machine execution and interaction with it.

[Important]Important

We expect user to be fairly familiar with a Spring Security meaning we don’t go into details of how overall security framework works. For this read Spring Security reference documentation.

First level of defence with a security is naturally protecting events which really are a driver from user point of view what is going to happen in a state machine. More fine grained security settings can then be defined for transitions and actions. This can be think of like allowing an employee to access a building, walk around it and then giving more detailed access rights to enter different rooms and allow to switch lights on and off while being on those rooms. If you trust your users then event security may be all you need, if you don’t, then more detailed security needs to be applied.

More detailed info can be found from section Section 24.6, “Understanding Security”.

[Tip]Tip

For complete example, see sample Chapter 44, Security.

24.1 Configuring Security

All generic configurations for security are done in SecurityConfigurer which is obtained from StateMachineConfigurationConfigurer. Security is disabled on default even if Spring Security classes are present.

@Configuration
@EnableStateMachine
static class Config4 extends StateMachineConfigurerAdapter<String, String> {

    @Override
    public void configure(StateMachineConfigurationConfigurer<String, String> config)
            throws Exception {
        config
            .withSecurity()
                .enabled(true)
                .transitionAccessDecisionManager(null)
                .eventAccessDecisionManager(null);
    }
}

If absolutely needed AccessDecisionManager for both events and transitions can be customised. If decision managers are not defined or are set to null, default managers are created internally.

24.2 Securing Events

Event security is defined on a global level within a SecurityConfigurer.

@Configuration
@EnableStateMachine
static class Config1 extends StateMachineConfigurerAdapter<String, String> {

    @Override
    public void configure(StateMachineConfigurationConfigurer<String, String> config)
            throws Exception {
        config
            .withSecurity()
                .enabled(true)
                .event("true")
                .event("ROLE_ANONYMOUS", ComparisonType.ANY);
    }
}

In above configuration we use expression true which always evaluates to TRUE. Using an expression which always evaluates to TRUE would not make sense in a real application but gives a point that expression needs to return either TRUE or FALSE. We also defined attribute ROLE_ANONYMOUS and ComparisonType ANY. Using attributes and expressions, see section Section 24.5, “Using Security Attributes and Expressions”.

24.3 Securing Transitions

Transition security can be defined globally.

@Configuration
@EnableStateMachine
static class Config6 extends StateMachineConfigurerAdapter<String, String> {

    @Override
    public void configure(StateMachineConfigurationConfigurer<String, String> config)
            throws Exception {
        config
            .withSecurity()
                .enabled(true)
                .transition("true")
                .transition("ROLE_ANONYMOUS", ComparisonType.ANY);
    }
}

If security is defined in a transition itself it will override any globally set security.

@Configuration
@EnableStateMachine
static class Config2 extends StateMachineConfigurerAdapter<String, String> {

    @Override
    public void configure(StateMachineTransitionConfigurer<String, String> transitions)
            throws Exception {
        transitions
            .withExternal()
                .source("S0")
                .target("S1")
                .event("A")
                .secured("ROLE_ANONYMOUS", ComparisonType.ANY)
                .secured("hasTarget('S1')");
    }
}

Using attributes and expressions, see section Section 24.5, “Using Security Attributes and Expressions”.

24.4 Securing Actions

There are no dedicated security definitions for actions in a state machine, but it can be accomplished using a global method security from a Spring Security. This simply needs that an Action is defined as a proxied @Bean and its execute method annotated with a @Secured.

@Configuration
@EnableStateMachine
static class Config3 extends StateMachineConfigurerAdapter<String, String> {

    @Override
    public void configure(StateMachineConfigurationConfigurer<String, String> config)
            throws Exception {
        config
            .withSecurity()
                .enabled(true);
    }

    @Override
    public void configure(StateMachineStateConfigurer<String, String> states)
            throws Exception {
        states
            .withStates()
                .initial("S0")
                .state("S1");
    }

    @Override
    public void configure(StateMachineTransitionConfigurer<String, String> transitions)
            throws Exception {
        transitions
            .withExternal()
                .source("S0")
                .target("S1")
                .action(securedAction())
                .event("A");
    }

    @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)
    @Bean
    public Action<String, String> securedAction() {
        return new Action<String, String>() {

            @Secured("ROLE_ANONYMOUS")
            @Override
            public void execute(StateContext<String, String> context) {
            }
        };
    }

}

Global method security needs to be enabled with a Spring Security which is done with along a lines shown below. See Spring Security reference docs for more details.

@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true)
public static class Config5 extends WebSecurityConfigurerAdapter {

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth
            .inMemoryAuthentication()
                .withUser("user").password("password").roles("USER");
    }
}

24.5 Using Security Attributes and Expressions

Generally there are two ways to define security properties, firstly using security attributes and secondly using security expressions. Attributes are easier to use but are relatively limited in terms of functionality. Expressions provide more features but are a little bit of harder to use.

24.5.1 Generic Attribute Usage

On default AccessDecisionManager instances for events and transitions both use a RoleVoter, meaning you can use role attributes familiar from Spring Security.

For attributes we have 3 different comparison types, ANY, ALL and MAJORITY which maps into default access decision managers AffirmativeBased, UnanimousBased and ConsensusBased respectively. If custom AccessDecisionManager has been defined, comparison type is effectively discarded as it’s only used to create a default manager.

24.5.2 Generic Expression Usage

Security expressions needs to return either TRUE or FALSE.

The base class for expression root objects is SecurityExpressionRoot. This provides some common expressions which are available in both transition and event security.

Table 24.1. Common built-in expressions

ExpressionDescription

hasRole([role])

Returns true if the current principal has the specified role. By default if the supplied role does not start with 'ROLE_' it will be added. This can be customized by modifying the defaultRolePrefix on DefaultWebSecurityExpressionHandler.

hasAnyRole([role1,role2])

Returns true if the current principal has any of the supplied roles (given as a comma-separated list of strings). By default if the supplied role does not start with 'ROLE_' it will be added. This can be customized by modifying the defaultRolePrefix on DefaultWebSecurityExpressionHandler.

hasAuthority([authority])

Returns true if the current principal has the specified authority.

hasAnyAuthority([authority1,authority2])

Returns true if the current principal has any of the supplied roles (given as a comma-separated list of strings)

principal

Allows direct access to the principal object representing the current user

authentication

Allows direct access to the current Authentication object obtained from the SecurityContext

permitAll

Always evaluates to true

denyAll

Always evaluates to false

isAnonymous()

Returns true if the current principal is an anonymous user

isRememberMe()

Returns true if the current principal is a remember-me user

isAuthenticated()

Returns true if the user is not anonymous

isFullyAuthenticated()

Returns true if the user is not an anonymous or a remember-me user

hasPermission(Object target, Object permission)

Returns true if the user has access to the provided target for the given permission. For example, hasPermission(domainObject, 'read')

hasPermission(Object targetId, String targetType, Object permission)

Returns true if the user has access to the provided target for the given permission. For example, hasPermission(1, 'com.example.domain.Message', 'read')


24.5.3 Event Attributes

Event id can be matched by using prefix EVENT_. For example matching event A would match with attribute EVENT_A.

24.5.4 Event Expressions

The base class for expression root object for event is EventSecurityExpressionRoot. This provides access to a Message object which is passed around with eventing.

Table 24.2. Event expressions

ExpressionDescription

hasEvent(Object event)

Returns true if the event matches given event.


24.5.5 Transition Attributes

Matching transition sources and targets, use prefixes TRANSITION_SOURCE_ and TRANSITION_TARGET_ respectively.

24.5.6 Transition Expressions

The base class for expression root object for transition is TransitionSecurityExpressionRoot. This provides access to a Transition object which is passed around for transition changes.

Table 24.3. Transition expressions

ExpressionDescription

hasSource(Object source)

Returns true if the transition source matches given source.

hasTarget(Object target)

Returns true if the transition target matches given target.


24.6 Understanding Security

This section provides more detailed info how security works within a state machine. Not really something you’d need to know but it is always better to be transparent instead of hiding all the magic what happens behind the scenes.

[Note]Note

Security only makes sense if State Machine is executed in a walled garden where user don’t have direct access to the application thus could modify Spring Security’s SecurityContext hold in a thread local. If user controls the jvm, then effectively there is no security at all.

Integration point for security is done with a StateMachineInterceptor which is then added automatically into a state machine if security is enabled. Specific class is a StateMachineSecurityInterceptor which intercepts events and transitions. This interceptor then consults Spring Security’s AccessDecisionManager if event can be send or if transition can be executed. Effectively if decision or vote with a AccessDecisionManager will result an exception, event or transition is denied.

Due to way how AccessDecisionManager from Spring Security works, we need one instance of it per secured object. This is a reason why there is a different manager for events and transitions. In this case events and transitions are different class objects we’re securing.

On default for events, voters EventExpressionVoter, EventVoter and RoleVoter are added into a AccessDecisionManager.

On default for transitions, voters TransitionExpressionVoter, TransitionVoter and RoleVoter are added into a AccessDecisionManager.