Add base security support

- Add base of support using spring security to
  protect events, transitions and actions.
- Fixes #114
This commit is contained in:
Janne Valkealahti
2015-12-20 14:33:54 +00:00
parent cba4b2fd0c
commit d0aaa4bfee
71 changed files with 4037 additions and 146 deletions

View File

@@ -24,6 +24,8 @@ import java.util.Map;
import java.util.Map.Entry;
import java.util.Stack;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.context.SmartLifecycle;
@@ -43,6 +45,7 @@ import org.springframework.statemachine.config.builders.StateMachineTransitions.
import org.springframework.statemachine.ensemble.DistributedStateMachine;
import org.springframework.statemachine.listener.StateMachineListener;
import org.springframework.statemachine.region.Region;
import org.springframework.statemachine.security.StateMachineSecurityInterceptor;
import org.springframework.statemachine.state.ChoicePseudoState;
import org.springframework.statemachine.state.ChoicePseudoState.ChoiceStateData;
import org.springframework.statemachine.state.DefaultPseudoState;
@@ -80,6 +83,8 @@ import org.springframework.util.ObjectUtils;
public abstract class AbstractStateMachineFactory<S, E> extends LifecycleObjectSupport implements
StateMachineFactory<S, E>, BeanNameAware {
private final Log log = LogFactory.getLog(AbstractStateMachineFactory.class);
private final StateMachineTransitions<S, E> stateMachineTransitions;
private final StateMachineStates<S, E> stateMachineStates;
@@ -105,7 +110,7 @@ public abstract class AbstractStateMachineFactory<S, E> extends LifecycleObjectS
this.stateMachineTransitions = stateMachineTransitions;
this.stateMachineStates = stateMachineStates;
}
@Override
public void setBeanName(String name) {
this.beanName = name;
@@ -204,7 +209,7 @@ public abstract class AbstractStateMachineFactory<S, E> extends LifecycleObjectS
if (machine instanceof LifecycleObjectSupport) {
((LifecycleObjectSupport)machine).setAutoStartup(stateMachineConfigurationConfig.isAutoStart());
}
// set top-level machine as relay
final StateMachine<S, E> fmachine = machine;
fmachine.getStateMachineAccessor().doWithAllRegions(new StateMachineFunction<StateMachineAccess<S, E>>() {
@@ -213,9 +218,24 @@ public abstract class AbstractStateMachineFactory<S, E> extends LifecycleObjectS
public void apply(StateMachineAccess<S, E> function) {
function.setRelay(fmachine);
}
});
// TODO: should error out if sec is enabled but spring-security is not in cp
if (stateMachineConfigurationConfig.isSecurityEnabled()) {
final StateMachineSecurityInterceptor<S, E> securityInterceptor = new StateMachineSecurityInterceptor<S, E>(
stateMachineConfigurationConfig.getTransitionSecurityAccessDecisionManager(),
stateMachineConfigurationConfig.getEventSecurityAccessDecisionManager(),
stateMachineConfigurationConfig.getEventSecurityRule());
log.info("Adding security interceptor " + securityInterceptor);
fmachine.getStateMachineAccessor().doWithAllRegions(new StateMachineFunction<StateMachineAccess<S, E>>() {
@Override
public void apply(StateMachineAccess<S, E> function) {
function.addStateMachineInterceptor(securityInterceptor);
}
});
}
// setup distributed state machine if needed.
// we wrap previously build machine with a distributed
// state machine and set it to use given ensemble.
@@ -533,12 +553,14 @@ public abstract class AbstractStateMachineFactory<S, E> extends LifecycleObjectS
continue;
}
DefaultExternalTransition<S, E> transition = new DefaultExternalTransition<S, E>(stateMap.get(source),
stateMap.get(target), transitionData.getActions(), event, transitionData.getGuard(), trigger);
stateMap.get(target), transitionData.getActions(), event, transitionData.getGuard(), trigger,
transitionData.getSecurityRule());
transitions.add(transition);
} else if (transitionData.getKind() == TransitionKind.INTERNAL) {
DefaultInternalTransition<S, E> transition = new DefaultInternalTransition<S, E>(stateMap.get(source),
transitionData.getActions(), event, transitionData.getGuard(), trigger);
transitionData.getActions(), event, transitionData.getGuard(), trigger,
transitionData.getSecurityRule());
transitions.add(transition);
}
}
@@ -569,10 +591,10 @@ public abstract class AbstractStateMachineFactory<S, E> extends LifecycleObjectS
}
TreeTraverser<Node<StateData<S, E>>> traverser = new TreeTraverser<Node<StateData<S, E>>>() {
@Override
public Iterable<Node<StateData<S, E>>> children(Node<StateData<S, E>> root) {
return root.getChildren();
}
@Override
public Iterable<Node<StateData<S, E>>> children(Node<StateData<S, E>> root) {
return root.getChildren();
}
};
Iterable<Node<StateData<S, E>>> postOrderTraversal = traverser.postOrderTraversal(tree.getRoot());

View File

@@ -25,12 +25,15 @@ public class StateMachineConfigBuilder<S, E>
@SuppressWarnings("unchecked")
@Override
protected StateMachineConfig<S, E> performBuild() throws Exception {
StateMachineTransitionBuilder<?, ?> sharedObject = getSharedObject(StateMachineTransitionBuilder.class);
StateMachineStateBuilder<?, ?> sharedObject2 = getSharedObject(StateMachineStateBuilder.class);
StateMachineConfigurationBuilder<?, ?> sharedObject3 = getSharedObject(StateMachineConfigurationBuilder.class);
StateMachineTransitions<S, E> transitions = (StateMachineTransitions<S, E>) sharedObject.build();
StateMachineStates<S, E> states = (StateMachineStates<S, E>) sharedObject2.build();
StateMachineConfigurationConfig<S, E> config = (StateMachineConfigurationConfig<S, E>) sharedObject3.build();
StateMachineConfigurationBuilder<?, ?> configurationBuilder = getSharedObject(StateMachineConfigurationBuilder.class);
StateMachineTransitionBuilder<?, ?> transitionBuilder = getSharedObject(StateMachineTransitionBuilder.class);
StateMachineStateBuilder<?, ?> stateBuilder = getSharedObject(StateMachineStateBuilder.class);
// build config first as it's shared with transition builder
StateMachineConfigurationConfig<S, E> config = (StateMachineConfigurationConfig<S, E>) configurationBuilder.build();
transitionBuilder.setSharedObject(StateMachineConfigurationConfig.class, config);
StateMachineTransitions<S, E> transitions = (StateMachineTransitions<S, E>) transitionBuilder.build();
StateMachineStates<S, E> states = (StateMachineStates<S, E>) stateBuilder.build();
StateMachineConfig<S, E> bean = new StateMachineConfig<S, E>(config, transitions, states);
return bean;
}

View File

@@ -21,15 +21,19 @@ import java.util.List;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.statemachine.config.common.annotation.AbstractConfiguredAnnotationBuilder;
import org.springframework.statemachine.config.common.annotation.AnnotationBuilder;
import org.springframework.statemachine.config.common.annotation.ObjectPostProcessor;
import org.springframework.statemachine.config.configurers.ConfigurationConfigurer;
import org.springframework.statemachine.config.configurers.DefaultConfigurationConfigurer;
import org.springframework.statemachine.config.configurers.DefaultDistributedStateMachineConfigurer;
import org.springframework.statemachine.config.configurers.DefaultSecurityConfigurer;
import org.springframework.statemachine.config.configurers.DistributedStateMachineConfigurer;
import org.springframework.statemachine.config.configurers.SecurityConfigurer;
import org.springframework.statemachine.ensemble.StateMachineEnsemble;
import org.springframework.statemachine.listener.StateMachineListener;
import org.springframework.statemachine.security.SecurityRule;
/**
* {@link AnnotationBuilder} for {@link StateMachineStates}.
@@ -49,6 +53,11 @@ public class StateMachineConfigurationBuilder<S, E>
private boolean autoStart = false;
private StateMachineEnsemble<S, E> ensemble;
private final List<StateMachineListener<S, E>> listeners = new ArrayList<StateMachineListener<S, E>>();
private boolean securityEnabled = false;
private AccessDecisionManager transitionSecurityAccessDecisionManager;
private AccessDecisionManager eventSecurityAccessDecisionManager;
private SecurityRule eventSecurityRule;
private SecurityRule transitionSecurityRule;
/**
* Instantiates a new state machine configuration builder.
@@ -87,9 +96,16 @@ public class StateMachineConfigurationBuilder<S, E>
return apply(new DefaultDistributedStateMachineConfigurer<S, E>());
}
@Override
public SecurityConfigurer<S, E> withSecurity() throws Exception {
return apply(new DefaultSecurityConfigurer<S, E>());
}
@Override
protected StateMachineConfigurationConfig<S, E> performBuild() throws Exception {
return new StateMachineConfigurationConfig<>(beanFactory, taskExecutor, taskScheculer, autoStart, ensemble, listeners);
return new StateMachineConfigurationConfig<S, E>(beanFactory, taskExecutor, taskScheculer, autoStart, ensemble, listeners,
securityEnabled, transitionSecurityAccessDecisionManager, eventSecurityAccessDecisionManager, eventSecurityRule,
transitionSecurityRule);
}
/**
@@ -147,4 +163,49 @@ public class StateMachineConfigurationBuilder<S, E>
this.listeners.addAll(listeners);
}
/**
* Sets the security enabled.
*
* @param securityEnabled the new security enabled
*/
public void setSecurityEnabled(boolean securityEnabled) {
this.securityEnabled = securityEnabled;
}
/**
* Sets the security transition access decision manager.
*
* @param transitionSecurityAccessDecisionManager the new security transition access decision manager
*/
public void setTransitionSecurityAccessDecisionManager(AccessDecisionManager transitionSecurityAccessDecisionManager) {
this.transitionSecurityAccessDecisionManager = transitionSecurityAccessDecisionManager;
}
/**
* Sets the security event access decision manager.
*
* @param eventSecurityAccessDecisionManager the new security event access decision manager
*/
public void setEventSecurityAccessDecisionManager(AccessDecisionManager eventSecurityAccessDecisionManager) {
this.eventSecurityAccessDecisionManager = eventSecurityAccessDecisionManager;
}
/**
* Sets the event security rule.
*
* @param eventSecurityRule the new event security rule
*/
public void setEventSecurityRule(SecurityRule eventSecurityRule) {
this.eventSecurityRule = eventSecurityRule;
}
/**
* Sets the transition security rule.
*
* @param transitionSecurityRule the new event security rule
*/
public void setTransitionSecurityRule(SecurityRule transitionSecurityRule) {
this.transitionSecurityRule = transitionSecurityRule;
}
}

View File

@@ -20,8 +20,10 @@ import java.util.List;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.statemachine.ensemble.StateMachineEnsemble;
import org.springframework.statemachine.listener.StateMachineListener;
import org.springframework.statemachine.security.SecurityRule;
/**
* Configuration object used to keep things together in {@link StateMachineConfigurationBuilder}.
@@ -39,6 +41,11 @@ public class StateMachineConfigurationConfig<S, E> {
private final boolean autoStart;
private final StateMachineEnsemble<S, E> ensemble;
private final List<StateMachineListener<S, E>> listeners;
private final boolean securityEnabled;
private final AccessDecisionManager transitionSecurityAccessDecisionManager;
private final AccessDecisionManager eventSecurityAccessDecisionManager;
private final SecurityRule eventSecurityRule;
private final SecurityRule transitionSecurityRule;
/**
* Instantiates a new state machine configuration config.
@@ -49,16 +56,28 @@ public class StateMachineConfigurationConfig<S, E> {
* @param autoStart the autostart flag
* @param ensemble the state machine ensemble
* @param listeners the state machine listeners
* @param securityEnabled the security enabled flag
* @param transitionSecurityAccessDecisionManager the transition security access decision manager
* @param eventSecurityAccessDecisionManager the event security access decision manager
* @param eventSecurityRule the event security rule
* @param transitionSecurityRule the transition security rule
*/
public StateMachineConfigurationConfig(BeanFactory beanFactory, TaskExecutor taskExecutor,
TaskScheduler taskScheduler, boolean autoStart, StateMachineEnsemble<S, E> ensemble,
List<StateMachineListener<S, E>> listeners) {
List<StateMachineListener<S, E>> listeners, boolean securityEnabled,
AccessDecisionManager transitionSecurityAccessDecisionManager, AccessDecisionManager eventSecurityAccessDecisionManager,
SecurityRule eventSecurityRule, SecurityRule transitionSecurityRule) {
this.beanFactory = beanFactory;
this.taskExecutor = taskExecutor;
this.taskScheduler = taskScheduler;
this.autoStart = autoStart;
this.ensemble = ensemble;
this.listeners = listeners;
this.securityEnabled = securityEnabled;
this.transitionSecurityAccessDecisionManager = transitionSecurityAccessDecisionManager;
this.eventSecurityAccessDecisionManager = eventSecurityAccessDecisionManager;
this.eventSecurityRule = eventSecurityRule;
this.transitionSecurityRule = transitionSecurityRule;
}
/**
@@ -115,4 +134,49 @@ public class StateMachineConfigurationConfig<S, E> {
return listeners;
}
/**
* Checks if is security is enabled.
*
* @return true, if is security is enabled
*/
public boolean isSecurityEnabled() {
return securityEnabled;
}
/**
* Gets the transition security access decision manager.
*
* @return the security access decision manager
*/
public AccessDecisionManager getTransitionSecurityAccessDecisionManager() {
return transitionSecurityAccessDecisionManager;
}
/**
* Gets the event security access decision manager.
*
* @return the event security access decision manager
*/
public AccessDecisionManager getEventSecurityAccessDecisionManager() {
return eventSecurityAccessDecisionManager;
}
/**
* Gets the event security rule.
*
* @return the event security rule
*/
public SecurityRule getEventSecurityRule() {
return eventSecurityRule;
}
/**
* Gets the transition security rule.
*
* @return the transition security rule
*/
public SecurityRule getTransitionSecurityRule() {
return transitionSecurityRule;
}
}

View File

@@ -17,6 +17,7 @@ package org.springframework.statemachine.config.builders;
import org.springframework.statemachine.config.configurers.ConfigurationConfigurer;
import org.springframework.statemachine.config.configurers.DistributedStateMachineConfigurer;
import org.springframework.statemachine.config.configurers.SecurityConfigurer;
/**
* Configurer interface exposing generic config.
@@ -44,4 +45,12 @@ public interface StateMachineConfigurationConfigurer<S, E> {
*/
DistributedStateMachineConfigurer<S, E> withDistributed() throws Exception;
/**
* Gets a configurer for securing state machine.
*
* @return {@link SecurityConfigurer} for chaining
* @throws Exception if configuration error happens
*/
SecurityConfigurer<S, E> withSecurity() throws Exception;
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.statemachine.config.builders;
import org.springframework.statemachine.config.configurers.SecurityConfigurer;
/**
* Configurer interface exposing generic config.
*
* @author Janne Valkealahti
*
* @param <S> the type of state
* @param <E> the type of event
*/
public interface StateMachineSecurityConfigurer<S, E> {
/**
* Gets a configurer for security.
*
* @return {@link SecurityConfigurer} for chaining
* @throws Exception if configuration error happens
*/
SecurityConfigurer<S, E> withSecurity() throws Exception;
}

View File

@@ -40,6 +40,7 @@ import org.springframework.statemachine.config.configurers.InternalTransitionCon
import org.springframework.statemachine.config.configurers.JoinTransitionConfigurer;
import org.springframework.statemachine.config.configurers.LocalTransitionConfigurer;
import org.springframework.statemachine.guard.Guard;
import org.springframework.statemachine.security.SecurityRule;
import org.springframework.statemachine.transition.TransitionKind;
/**
@@ -109,8 +110,14 @@ public class StateMachineTransitionBuilder<S, E>
}
public void add(S source, S target, S state, E event, Long period, Collection<Action<S, E>> actions,
Guard<S, E> guard, TransitionKind kind) {
transitionData.add(new TransitionData<S, E>(source, target, state, event, period, actions, guard, kind));
Guard<S, E> guard, TransitionKind kind, SecurityRule securityRule) {
// if rule not given, get it from global
if (securityRule == null) {
@SuppressWarnings("unchecked")
StateMachineConfigurationConfig<S, E> config = getSharedObject(StateMachineConfigurationConfig.class);
securityRule = config.getTransitionSecurityRule();
}
transitionData.add(new TransitionData<S, E>(source, target, state, event, period, actions, guard, kind, securityRule));
}
public void add(S source, List<ChoiceData<S, E>> choices) {

View File

@@ -21,6 +21,7 @@ import java.util.Map;
import org.springframework.statemachine.action.Action;
import org.springframework.statemachine.guard.Guard;
import org.springframework.statemachine.security.SecurityRule;
import org.springframework.statemachine.transition.TransitionKind;
/**
@@ -105,6 +106,7 @@ public class StateMachineTransitions<S, E> {
private final Collection<Action<S, E>> actions;
private final Guard<S, E> guard;
private final TransitionKind kind;
private final SecurityRule securityRule;
/**
* Instantiates a new transition data.
@@ -117,9 +119,10 @@ public class StateMachineTransitions<S, E> {
* @param actions the actions
* @param guard the guard
* @param kind the kind
* @param securityRule the security rule
*/
public TransitionData(S source, S target, S state, E event, Long period, Collection<Action<S, E>> actions,
Guard<S, E> guard, TransitionKind kind) {
Guard<S, E> guard, TransitionKind kind, SecurityRule securityRule) {
this.source = source;
this.target = target;
this.state = state;
@@ -128,6 +131,7 @@ public class StateMachineTransitions<S, E> {
this.actions = actions;
this.guard = guard;
this.kind = kind;
this.securityRule = securityRule;
}
/**
@@ -201,6 +205,15 @@ public class StateMachineTransitions<S, E> {
public TransitionKind getKind() {
return kind;
}
/**
* Gets the security rule.
*
* @return the security rule
*/
public SecurityRule getSecurityRule() {
return securityRule;
}
}
/**

View File

@@ -0,0 +1,124 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.statemachine.config.configurers;
import java.util.ArrayList;
import java.util.Collection;
import org.springframework.statemachine.action.Action;
import org.springframework.statemachine.config.builders.StateMachineTransitionBuilder;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitions;
import org.springframework.statemachine.config.common.annotation.AnnotationConfigurerAdapter;
import org.springframework.statemachine.guard.Guard;
import org.springframework.statemachine.security.SecurityRule;
import org.springframework.statemachine.security.SecurityRule.ComparisonType;
/**
* Base class for transition configurers.
*
* @author Janne Valkealahti
*
* @param <S> the type of state
* @param <E> the type of event
*/
public abstract class AbstractTransitionConfigurer<S, E> extends
AnnotationConfigurerAdapter<StateMachineTransitions<S, E>, StateMachineTransitionConfigurer<S, E>, StateMachineTransitionBuilder<S, E>> {
private S source;
private S target;
private S state;
private E event;
private Long period;
private final Collection<Action<S, E>> actions = new ArrayList<Action<S, E>>();
private Guard<S, E> guard;
private SecurityRule securityRule;
protected S getSource() {
return source;
}
protected S getTarget() {
return target;
}
protected S getState() {
return state;
}
protected E getEvent() {
return event;
}
protected Long getPeriod() {
return period;
}
protected Collection<Action<S, E>> getActions() {
return actions;
}
protected Guard<S, E> getGuard() {
return guard;
}
protected SecurityRule getSecurityRule() {
return securityRule;
}
protected void setSource(S source) {
this.source = source;
}
protected void setTarget(S target) {
this.target = target;
}
protected void setState(S state) {
this.state = state;
}
protected void setEvent(E event) {
this.event = event;
}
protected void setPeriod(long period) {
this.period = period;
}
protected void addAction(Action<S, E> action) {
this.actions.add(action);
}
protected void setGuard(Guard<S, E> guard) {
this.guard = guard;
}
protected void setSecurityRule(String attributes, ComparisonType match) {
if (securityRule == null) {
securityRule = new SecurityRule();
}
securityRule.setAttributes(SecurityRule.commaDelimitedListToSecurityAttributes(attributes));
}
protected void setSecurityRule(String expression) {
if (securityRule == null) {
securityRule = new SecurityRule();
}
securityRule.setExpression(expression);
}
}

View File

@@ -15,19 +15,14 @@
*/
package org.springframework.statemachine.config.configurers;
import java.util.ArrayList;
import java.util.Collection;
import org.springframework.expression.spel.SpelCompilerMode;
import org.springframework.expression.spel.SpelParserConfiguration;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.statemachine.action.Action;
import org.springframework.statemachine.config.builders.StateMachineTransitionBuilder;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitions;
import org.springframework.statemachine.config.common.annotation.AnnotationConfigurerAdapter;
import org.springframework.statemachine.guard.Guard;
import org.springframework.statemachine.guard.SpelExpressionGuard;
import org.springframework.statemachine.security.SecurityRule.ComparisonType;
import org.springframework.statemachine.transition.TransitionKind;
/**
@@ -38,68 +33,54 @@ import org.springframework.statemachine.transition.TransitionKind;
* @param <S> the type of state
* @param <E> the type of event
*/
public class DefaultExternalTransitionConfigurer<S, E>
extends AnnotationConfigurerAdapter<StateMachineTransitions<S, E>, StateMachineTransitionConfigurer<S, E>, StateMachineTransitionBuilder<S, E>>
public class DefaultExternalTransitionConfigurer<S, E> extends AbstractTransitionConfigurer<S, E>
implements ExternalTransitionConfigurer<S, E> {
private S source;
private S target;
private S state;
private E event;
private Long period;
private Collection<Action<S, E>> actions = new ArrayList<Action<S, E>>();
private Guard<S, E> guard;
@Override
public void configure(StateMachineTransitionBuilder<S, E> builder) throws Exception {
builder.add(source, target, state, event, period, actions, guard, TransitionKind.EXTERNAL);
builder.add(getSource(), getTarget(), getState(), getEvent(), getPeriod(), getActions(), getGuard(), TransitionKind.EXTERNAL,
getSecurityRule());
}
@Override
public ExternalTransitionConfigurer<S, E> source(S source) {
this.source = source;
setSource(source);
return this;
}
@Override
public ExternalTransitionConfigurer<S, E> target(S target) {
this.target = target;
setTarget(target);
return this;
}
@Override
public ExternalTransitionConfigurer<S, E> state(S state) {
this.state = state;
setState(state);
return this;
}
@Override
public ExternalTransitionConfigurer<S, E> event(E event) {
this.event = event;
setEvent(event);
return this;
}
@Override
public ExternalTransitionConfigurer<S, E> timer(long period) {
this.period = period;
setPeriod(period);
return this;
}
@Override
public ExternalTransitionConfigurer<S, E> action(Action<S, E> action) {
actions.add(action);
addAction(action);
return this;
}
@Override
public ExternalTransitionConfigurer<S, E> guard(Guard<S, E> guard) {
this.guard = guard;
setGuard(guard);
return this;
}
@@ -107,7 +88,19 @@ public class DefaultExternalTransitionConfigurer<S, E>
public ExternalTransitionConfigurer<S, E> guardExpression(String expression) {
SpelExpressionParser parser = new SpelExpressionParser(
new SpelParserConfiguration(SpelCompilerMode.MIXED, null));
this.guard = new SpelExpressionGuard<S, E>(parser.parseExpression(expression));
setGuard(new SpelExpressionGuard<S, E>(parser.parseExpression(expression)));
return this;
}
@Override
public ExternalTransitionConfigurer<S, E> secured(String attributes, ComparisonType match) {
setSecurityRule(attributes, match);
return this;
}
@Override
public ExternalTransitionConfigurer<S, E> secured(String expression) {
setSecurityRule(expression);
return this;
}

View File

@@ -15,19 +15,14 @@
*/
package org.springframework.statemachine.config.configurers;
import java.util.ArrayList;
import java.util.Collection;
import org.springframework.expression.spel.SpelCompilerMode;
import org.springframework.expression.spel.SpelParserConfiguration;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.statemachine.action.Action;
import org.springframework.statemachine.config.builders.StateMachineTransitionBuilder;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitions;
import org.springframework.statemachine.config.common.annotation.AnnotationConfigurerAdapter;
import org.springframework.statemachine.guard.Guard;
import org.springframework.statemachine.guard.SpelExpressionGuard;
import org.springframework.statemachine.security.SecurityRule.ComparisonType;
import org.springframework.statemachine.transition.TransitionKind;
/**
@@ -38,62 +33,48 @@ import org.springframework.statemachine.transition.TransitionKind;
* @param <S> the type of state
* @param <E> the type of event
*/
public class DefaultInternalTransitionConfigurer<S, E>
extends AnnotationConfigurerAdapter<StateMachineTransitions<S, E>, StateMachineTransitionConfigurer<S, E>, StateMachineTransitionBuilder<S, E>>
public class DefaultInternalTransitionConfigurer<S, E> extends AbstractTransitionConfigurer<S, E>
implements InternalTransitionConfigurer<S, E> {
private S source;
private S target;
private S state;
private E event;
private Long period;
private Collection<Action<S, E>> actions = new ArrayList<Action<S, E>>();
private Guard<S, E> guard;
@Override
public void configure(StateMachineTransitionBuilder<S, E> builder) throws Exception {
builder.add(source, target, state, event, period, actions, guard, TransitionKind.INTERNAL);
builder.add(getSource(), getTarget(), getState(), getEvent(), getPeriod(), getActions(), getGuard(), TransitionKind.INTERNAL,
getSecurityRule());
}
@Override
public InternalTransitionConfigurer<S, E> source(S source) {
this.source = source;
setSource(source);
return this;
}
@Override
public InternalTransitionConfigurer<S, E> state(S state) {
this.state = state;
setState(state);
return this;
}
@Override
public InternalTransitionConfigurer<S, E> event(E event) {
this.event = event;
setEvent(event);
return this;
}
@Override
public InternalTransitionConfigurer<S, E> timer(long period) {
this.period = period;
setPeriod(period);
return this;
}
@Override
public InternalTransitionConfigurer<S, E> action(Action<S, E> action) {
actions.add(action);
addAction(action);
return this;
}
@Override
public InternalTransitionConfigurer<S, E> guard(Guard<S, E> guard) {
this.guard = guard;
setGuard(guard);
return this;
}
@@ -101,7 +82,19 @@ public class DefaultInternalTransitionConfigurer<S, E>
public InternalTransitionConfigurer<S, E> guardExpression(String expression) {
SpelExpressionParser parser = new SpelExpressionParser(
new SpelParserConfiguration(SpelCompilerMode.MIXED, null));
this.guard = new SpelExpressionGuard<S, E>(parser.parseExpression(expression));
setGuard(new SpelExpressionGuard<S, E>(parser.parseExpression(expression)));
return this;
}
@Override
public InternalTransitionConfigurer<S, E> secured(String attributes, ComparisonType match) {
setSecurityRule(attributes, match);
return this;
}
@Override
public InternalTransitionConfigurer<S, E> secured(String expression) {
setSecurityRule(expression);
return this;
}

View File

@@ -15,19 +15,14 @@
*/
package org.springframework.statemachine.config.configurers;
import java.util.ArrayList;
import java.util.Collection;
import org.springframework.expression.spel.SpelCompilerMode;
import org.springframework.expression.spel.SpelParserConfiguration;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.statemachine.action.Action;
import org.springframework.statemachine.config.builders.StateMachineTransitionBuilder;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitions;
import org.springframework.statemachine.config.common.annotation.AnnotationConfigurerAdapter;
import org.springframework.statemachine.guard.Guard;
import org.springframework.statemachine.guard.SpelExpressionGuard;
import org.springframework.statemachine.security.SecurityRule.ComparisonType;
import org.springframework.statemachine.transition.TransitionKind;
/**
@@ -38,68 +33,53 @@ import org.springframework.statemachine.transition.TransitionKind;
* @param <S> the type of state
* @param <E> the type of event
*/
public class DefaultLocalTransitionConfigurer<S, E>
extends AnnotationConfigurerAdapter<StateMachineTransitions<S, E>, StateMachineTransitionConfigurer<S, E>, StateMachineTransitionBuilder<S, E>>
implements LocalTransitionConfigurer<S, E> {
private S source;
private S target;
private S state;
private E event;
private Long period;
private Collection<Action<S, E>> actions = new ArrayList<Action<S, E>>();
private Guard<S, E> guard;
public class DefaultLocalTransitionConfigurer<S, E> extends AbstractTransitionConfigurer<S, E> implements LocalTransitionConfigurer<S, E> {
@Override
public void configure(StateMachineTransitionBuilder<S, E> builder) throws Exception {
builder.add(source, target, state, event, period, actions, guard, TransitionKind.LOCAL);
builder.add(getSource(), getTarget(), getState(), getEvent(), getPeriod(), getActions(), getGuard(), TransitionKind.LOCAL,
getSecurityRule());
}
@Override
public LocalTransitionConfigurer<S, E> source(S source) {
this.source = source;
setSource(source);
return this;
}
@Override
public LocalTransitionConfigurer<S, E> target(S target) {
this.target = target;
setTarget(target);
return this;
}
@Override
public LocalTransitionConfigurer<S, E> state(S state) {
this.state = state;
setState(state);
return this;
}
@Override
public LocalTransitionConfigurer<S, E> event(E event) {
this.event = event;
setEvent(event);
return this;
}
@Override
public LocalTransitionConfigurer<S, E> timer(long period) {
this.period = period;
setPeriod(period);
return this;
}
@Override
public LocalTransitionConfigurer<S, E> action(Action<S, E> action) {
actions.add(action);
addAction(action);
return this;
}
@Override
public LocalTransitionConfigurer<S, E> guard(Guard<S, E> guard) {
this.guard = guard;
setGuard(guard);
return this;
}
@@ -107,7 +87,19 @@ public class DefaultLocalTransitionConfigurer<S, E>
public LocalTransitionConfigurer<S, E> guardExpression(String expression) {
SpelExpressionParser parser = new SpelExpressionParser(
new SpelParserConfiguration(SpelCompilerMode.MIXED, null));
this.guard = new SpelExpressionGuard<S, E>(parser.parseExpression(expression));
setGuard(new SpelExpressionGuard<S, E>(parser.parseExpression(expression)));
return this;
}
@Override
public LocalTransitionConfigurer<S, E> secured(String attributes, ComparisonType match) {
setSecurityRule(attributes, match);
return this;
}
@Override
public LocalTransitionConfigurer<S, E> secured(String expression) {
setSecurityRule(expression);
return this;
}

View File

@@ -0,0 +1,109 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.statemachine.config.configurers;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.statemachine.config.builders.StateMachineConfigurationBuilder;
import org.springframework.statemachine.config.builders.StateMachineConfigurationConfig;
import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer;
import org.springframework.statemachine.config.common.annotation.AnnotationConfigurerAdapter;
import org.springframework.statemachine.security.SecurityRule;
import org.springframework.statemachine.security.SecurityRule.ComparisonType;
/**
* Default implementation of a {@link SecurityConfigurer}.
*
* @author Janne Valkealahti
*
* @param <S> the type of state
* @param <E> the type of event
*/
public class DefaultSecurityConfigurer<S, E>
extends AnnotationConfigurerAdapter<StateMachineConfigurationConfig<S, E>, StateMachineConfigurationConfigurer<S, E>, StateMachineConfigurationBuilder<S, E>>
implements SecurityConfigurer<S, E> {
private boolean enabled = true;
private AccessDecisionManager transitionAccessDecisionManager;
private AccessDecisionManager eventAccessDecisionManager;
private SecurityRule eventSecurityRule;
private SecurityRule transitionSecurityRule;
@Override
public void configure(StateMachineConfigurationBuilder<S, E> builder) throws Exception {
if (enabled) {
builder.setSecurityEnabled(true);
builder.setTransitionSecurityAccessDecisionManager(transitionAccessDecisionManager);
builder.setEventSecurityAccessDecisionManager(eventAccessDecisionManager);
builder.setEventSecurityRule(eventSecurityRule);
builder.setTransitionSecurityRule(transitionSecurityRule);
}
}
@Override
public SecurityConfigurer<S, E> enabled(boolean enabled) {
this.enabled = enabled;
return this;
}
@Override
public SecurityConfigurer<S, E> transitionAccessDecisionManager(AccessDecisionManager accessDecisionManager) {
this.transitionAccessDecisionManager = accessDecisionManager;
return this;
}
@Override
public SecurityConfigurer<S, E> eventAccessDecisionManager(AccessDecisionManager accessDecisionManager) {
this.eventAccessDecisionManager = accessDecisionManager;
return this;
}
@Override
public SecurityConfigurer<S, E> event(String attributes, ComparisonType match) {
if (eventSecurityRule == null) {
eventSecurityRule = new SecurityRule();
}
eventSecurityRule.setAttributes(SecurityRule.commaDelimitedListToSecurityAttributes(attributes));
return this;
}
@Override
public SecurityConfigurer<S, E> event(String expression) {
if (eventSecurityRule == null) {
eventSecurityRule = new SecurityRule();
}
eventSecurityRule.setExpression(expression);
return this;
}
@Override
public SecurityConfigurer<S, E> transition(String attributes, ComparisonType match) {
if (transitionSecurityRule == null) {
transitionSecurityRule = new SecurityRule();
}
transitionSecurityRule.setAttributes(SecurityRule.commaDelimitedListToSecurityAttributes(attributes));
return this;
}
@Override
public SecurityConfigurer<S, E> transition(String expression) {
if (transitionSecurityRule == null) {
transitionSecurityRule = new SecurityRule();
}
transitionSecurityRule.setExpression(expression);
return this;
}
}

View File

@@ -19,7 +19,7 @@ import org.springframework.statemachine.transition.Transition;
/**
* {@code TransitionConfigurer} interface for configuring external {@link Transition}s.
*
*
* @author Janne Valkealahti
*
* @param <S> the type of state

View File

@@ -0,0 +1,93 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.statemachine.config.configurers;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer;
import org.springframework.statemachine.config.common.annotation.AnnotationConfigurerBuilder;
import org.springframework.statemachine.security.SecurityRule.ComparisonType;
/**
* Base {@code ConfigConfigurer} interface for configuring generic config.
*
* @author Janne Valkealahti
*
* @param <S> the type of state
* @param <E> the type of event
*/
public interface SecurityConfigurer<S, E> extends
AnnotationConfigurerBuilder<StateMachineConfigurationConfigurer<S, E>> {
/**
* Specify if security is enabled. On default security is enabled
* if configurer is used.
*
* @param enabled the enable flag
* @return configurer for chaining
*/
SecurityConfigurer<S, E> enabled(boolean enabled);
/**
* Specify a custom {@link AccessDecisionManager} for transitions.
*
* @param accessDecisionManager the access decision manager
* @return configurer for chaining
*/
SecurityConfigurer<S, E> transitionAccessDecisionManager(AccessDecisionManager accessDecisionManager);
/**
* Specify a custom {@link AccessDecisionManager} for events.
*
* @param accessDecisionManager the access decision manager
* @return configurer for chaining
*/
SecurityConfigurer<S, E> eventAccessDecisionManager(AccessDecisionManager accessDecisionManager);
/**
* Specify a security attributes for events.
*
* @param attributes the security attributes
* @param match the match type
* @return configurer for chaining
*/
SecurityConfigurer<S, E> event(String attributes, ComparisonType match);
/**
* Specify a security attributes for events.
*
* @param expression the the security expression
* @return configurer for chaining
*/
SecurityConfigurer<S, E> event(String expression);
/**
* Specify a security attributes for transitions.
*
* @param attributes the security attributes
* @param match the match type
* @return configurer for chaining
*/
SecurityConfigurer<S, E> transition(String attributes, ComparisonType match);
/**
* Specify a security attributes for transitions.
*
* @param expression the the security expression
* @return configurer for chaining
*/
SecurityConfigurer<S, E> transition(String expression);
}

View File

@@ -19,6 +19,7 @@ import org.springframework.statemachine.action.Action;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
import org.springframework.statemachine.config.common.annotation.AnnotationConfigurerBuilder;
import org.springframework.statemachine.guard.Guard;
import org.springframework.statemachine.security.SecurityRule.ComparisonType;
import org.springframework.statemachine.transition.Transition;
/**
@@ -43,7 +44,7 @@ public interface TransitionConfigurer<T, S, E> extends
/**
* Specify a state this transition should belong to.
*
*
* @param state the state {@code S}
* @return configurer for chaining
*/
@@ -60,7 +61,7 @@ public interface TransitionConfigurer<T, S, E> extends
/**
* Specify that this transition is triggered by a time.
*
*
* @param period timer period in millis
* @return configurer for chaining
*/
@@ -90,4 +91,22 @@ public interface TransitionConfigurer<T, S, E> extends
*/
T guardExpression(String expression);
/**
* Specify a security attributes for this {@link Transition}.
*
* @param attributes the security attributes
* @param match the match type
* @return configurer for chaining
*/
T secured(String attributes, ComparisonType match);
/**
* Specify a security expression for this {@link Transition}.
*
* @param expression the security expression
* @return configurer for chaining
*/
T secured(String expression);
}

View File

@@ -185,6 +185,11 @@ public class DistributedStateMachine<S, E> extends LifecycleObjectSupport implem
*/
private class LocalStateMachineInterceptor implements StateMachineInterceptor<S, E> {
@Override
public Message<E> preEvent(Message<E> message, StateMachine<S, E> stateMachine) {
return message;
}
@Override
public void preStateChange(State<S, E> state, Message<E> message, Transition<S, E> transition,
StateMachine<S, E> stateMachine) {

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.statemachine.security;
import org.springframework.messaging.Message;
import org.springframework.security.access.expression.AbstractSecurityExpressionHandler;
import org.springframework.security.access.expression.SecurityExpressionHandler;
import org.springframework.security.access.expression.SecurityExpressionOperations;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.core.Authentication;
import org.springframework.util.Assert;
/**
* The default implementation of {@link SecurityExpressionHandler} which uses a
* {@link EventSecurityExpressionRoot}.
*
* @param <T> the type for the body of the Message
*
* @author Rob Winch
* @author Janne Valkealahti
*/
public class DefaultEventSecurityExpressionHandler<T> extends
AbstractSecurityExpressionHandler<Message<T>> {
private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
@Override
protected SecurityExpressionOperations createSecurityExpressionRoot(
Authentication authentication, Message<T> invocation) {
EventSecurityExpressionRoot root = new EventSecurityExpressionRoot(
authentication, invocation);
root.setPermissionEvaluator(getPermissionEvaluator());
root.setTrustResolver(trustResolver);
root.setRoleHierarchy(getRoleHierarchy());
return root;
}
public void setTrustResolver(AuthenticationTrustResolver trustResolver) {
Assert.notNull(trustResolver, "trustResolver cannot be null");
this.trustResolver = trustResolver;
}
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.statemachine.security;
import org.springframework.security.access.expression.AbstractSecurityExpressionHandler;
import org.springframework.security.access.expression.SecurityExpressionHandler;
import org.springframework.security.access.expression.SecurityExpressionOperations;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.core.Authentication;
import org.springframework.statemachine.transition.Transition;
import org.springframework.util.Assert;
public class DefaultTransitionSecurityExpressionHandler extends AbstractSecurityExpressionHandler<Transition<?, ?>>
implements SecurityExpressionHandler<Transition<?, ?>> {
private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
private String defaultRolePrefix = "ROLE_";
@Override
protected SecurityExpressionOperations createSecurityExpressionRoot(Authentication authentication, Transition<?, ?> transition) {
TransitionSecurityExpressionRoot root = new TransitionSecurityExpressionRoot(authentication, transition);
root.setPermissionEvaluator(getPermissionEvaluator());
root.setTrustResolver(trustResolver);
root.setRoleHierarchy(getRoleHierarchy());
root.setDefaultRolePrefix(defaultRolePrefix);
return root;
}
/**
* Sets the {@link AuthenticationTrustResolver} to be used. The default is
* {@link AuthenticationTrustResolverImpl}.
*
* @param trustResolver
* the {@link AuthenticationTrustResolver} to use. Cannot be
* null.
*/
public void setTrustResolver(AuthenticationTrustResolver trustResolver) {
Assert.notNull(trustResolver, "trustResolver cannot be null");
this.trustResolver = trustResolver;
}
/**
* <p>
* Sets the default prefix to be added to
* {@link org.springframework.security.access.expression.SecurityExpressionRoot#hasAnyRole(String...)}
* or
* {@link org.springframework.security.access.expression.SecurityExpressionRoot#hasRole(String)}
* . For example, if hasRole("ADMIN") or hasRole("ROLE_ADMIN") is passed in,
* then the role ROLE_ADMIN will be used when the defaultRolePrefix is
* "ROLE_" (default).
* </p>
*
* <p>
* If null or empty, then no default role prefix is used.
* </p>
*
* @param defaultRolePrefix
* the default prefix to add to roles. Default "ROLE_".
*/
public void setDefaultRolePrefix(String defaultRolePrefix) {
this.defaultRolePrefix = defaultRolePrefix;
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.statemachine.security;
import org.springframework.expression.Expression;
import org.springframework.messaging.Message;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.util.Assert;
/**
* Simple expression configuration attribute for use in {@link Message} authorizations.
*
* @author Rob Winch
* @author Janne Valkealahti
*/
@SuppressWarnings("serial")
class EventExpressionConfigAttribute implements ConfigAttribute {
private final Expression authorizeExpression;
/**
* Creates a new instance
*
* @param authorizeExpression the {@link Expression} to use. Cannot be null
*/
public EventExpressionConfigAttribute(Expression authorizeExpression) {
Assert.notNull(authorizeExpression, "authorizeExpression cannot be null");
this.authorizeExpression = authorizeExpression;
}
Expression getAuthorizeExpression() {
return authorizeExpression;
}
public String getAttribute() {
return null;
}
@Override
public String toString() {
return authorizeExpression.getExpressionString();
}
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.statemachine.security;
import org.springframework.expression.EvaluationContext;
import org.springframework.messaging.Message;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.expression.ExpressionUtils;
import org.springframework.security.access.expression.SecurityExpressionHandler;
import org.springframework.security.core.Authentication;
import org.springframework.util.Assert;
import java.util.Collection;
/**
* Voter which handles {@link Message} authorisation decisions. If a
* {@link EventExpressionConfigAttribute} is found, then its expression is evaluated. If
* true, {@code ACCESS_GRANTED} is returned. If false, {@code ACCESS_DENIED} is returned.
* If no {@code MessageExpressionConfigAttribute} is found, then {@code ACCESS_ABSTAIN} is
* returned.
*
* @author Rob Winch
* @author Janne Valkealahti
*/
public class EventExpressionVoter<T> implements AccessDecisionVoter<Message<T>> {
private SecurityExpressionHandler<Message<T>> expressionHandler = new DefaultEventSecurityExpressionHandler<T>();
public int vote(Authentication authentication, Message<T> message,
Collection<ConfigAttribute> attributes) {
assert authentication != null;
assert message != null;
assert attributes != null;
EventExpressionConfigAttribute attr = findConfigAttribute(attributes);
if (attr == null) {
return ACCESS_ABSTAIN;
}
EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication,
message);
return ExpressionUtils.evaluateAsBoolean(attr.getAuthorizeExpression(), ctx) ? ACCESS_GRANTED
: ACCESS_DENIED;
}
private EventExpressionConfigAttribute findConfigAttribute(
Collection<ConfigAttribute> attributes) {
for (ConfigAttribute attribute : attributes) {
if (attribute instanceof EventExpressionConfigAttribute) {
return (EventExpressionConfigAttribute) attribute;
}
}
return null;
}
public boolean supports(ConfigAttribute attribute) {
return attribute instanceof EventExpressionConfigAttribute;
}
public boolean supports(Class<?> clazz) {
boolean foo = Message.class.isAssignableFrom(clazz);
return foo;
// return Message.class.isAssignableFrom(clazz);
}
public void setExpressionHandler(
SecurityExpressionHandler<Message<T>> expressionHandler) {
Assert.notNull(expressionHandler, "expressionHandler cannot be null");
this.expressionHandler = expressionHandler;
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.statemachine.security;
import org.springframework.messaging.Message;
import org.springframework.security.access.expression.SecurityExpressionRoot;
import org.springframework.security.core.Authentication;
/**
* The {@link SecurityExpressionRoot} used for {@link Message} expressions.
*
* @author Rob Winch
* @author Janne Valkealahti
*/
public class EventSecurityExpressionRoot extends SecurityExpressionRoot {
public final Message<?> message;
public EventSecurityExpressionRoot(Authentication authentication, Message<?> message) {
super(authentication);
this.message = message;
}
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.statemachine.security;
import java.util.Collection;
import org.springframework.messaging.Message;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.core.Authentication;
/**
* Votes if any {@link ConfigAttribute#getAttribute()} starts with a prefix indicating
* that it is an event. The default prefix is <Code>EVENT</code>, but
* it may be overridden to any value. It may also be set to empty, which means that
* essentially any attribute will be voted on. As described further below, the effect
* of an empty prefix may not be quite desirable.
* <p>
* All comparisons and prefixes are case sensitive.
*
* @author Janne Valkealahti
*
* @param <T> the message type
*/
public class EventVoter<T> implements AccessDecisionVoter<Message<T>>{
private String eventPrefix = "EVENT_";
@Override
public boolean supports(ConfigAttribute attribute) {
if ((attribute.getAttribute() != null) && attribute.getAttribute().startsWith(getEventPrefix())) {
return true;
} else {
return false;
}
}
@Override
public boolean supports(Class<?> clazz) {
return Message.class.isAssignableFrom(clazz);
}
@Override
public int vote(Authentication authentication, Message<T> event, Collection<ConfigAttribute> attributes) {
int result = ACCESS_ABSTAIN;
if (authentication == null) {
return result;
}
T e = event.getPayload();
for (ConfigAttribute attribute : attributes) {
if (this.supports(attribute)) {
result = ACCESS_DENIED;
String attr = attribute.getAttribute();
if (attr.startsWith(getEventPrefix()) && attr.equals(getEventPrefix() + e.toString())) {
return ACCESS_GRANTED;
}
}
}
return result;
}
/**
* Gets the event prefix.
*
* @return the event prefix
*/
public String getEventPrefix() {
return eventPrefix;
}
}

View File

@@ -0,0 +1,141 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.statemachine.security;
import java.util.Collection;
import java.util.HashSet;
import org.springframework.util.StringUtils;
/**
* Encapsulates the rules for comparing security attributes and expression.
*
* @author Janne Valkealahti
*/
public class SecurityRule {
private Collection<String> attributes;
private ComparisonType comparisonType = ComparisonType.ANY;
private String expression;
/**
* Convert attributes to comma separated String
*
* @param attributes the attributes to convert
* @return comma separated String
*/
public static String securityAttributesToCommaDelimitedList(Collection<?> attributes) {
return StringUtils.collectionToDelimitedString(attributes, ", ");
}
/**
* Convert attributes from comma separated String to Collection
*
* @param attributes the attributes to convert
* @return comma parsed Collection
*/
public static Collection<String> commaDelimitedListToSecurityAttributes(String attributes) {
Collection<String> attrs = new HashSet<String>();
for (String attribute : attributes.split(",")) {
attribute = attribute.trim();
if (!"".equals(attribute)) {
attrs.add(attribute);
}
}
return attrs;
}
/**
* Gets the security attributes.
*
* @return the security attributes
*/
public Collection<String> getAttributes() {
return attributes;
}
/**
* Sets the security attributes.
*
* @param attributes the new security attributes
*/
public void setAttributes(Collection<String> attributes) {
this.attributes = attributes;
}
/**
* Gets the comparison type.
*
* @return the comparison type
*/
public ComparisonType getComparisonType() {
return comparisonType;
}
/**
* Sets the comparison type.
*
* @param comparisonType the new comparison type
*/
public void setComparisonType(ComparisonType comparisonType) {
this.comparisonType = comparisonType;
}
/**
* Gets the security expression.
*
* @return the security expression
*/
public String getExpression() {
return expression;
}
/**
* Sets the security expression.
*
* @param expression the new security expression
*/
public void setExpression(String expression) {
this.expression = expression;
}
/**
* Security comparison types.
*/
public static enum ComparisonType {
/**
* Compare method where any attribute authorization allows access
*/
ANY,
/**
* Compare method where all attribute authorization allows access
*/
ALL,
/**
* Compare method where majority attribute authorization allows access
*/
MAJORITY;
}
@Override
public String toString() {
return "SecurityRule [attributes=" + attributes + ", comparisonType=" + comparisonType + ", expression=" + expression + "]";
}
}

View File

@@ -0,0 +1,239 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.statemachine.security;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.SpelCompilerMode;
import org.springframework.expression.spel.SpelParserConfiguration;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.messaging.Message;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.access.vote.AbstractAccessDecisionManager;
import org.springframework.security.access.vote.AffirmativeBased;
import org.springframework.security.access.vote.ConsensusBased;
import org.springframework.security.access.vote.RoleVoter;
import org.springframework.security.access.vote.UnanimousBased;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.statemachine.StateContext;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.support.StateMachineInterceptor;
import org.springframework.statemachine.support.StateMachineInterceptorAdapter;
import org.springframework.statemachine.transition.Transition;
import org.springframework.util.StringUtils;
/**
* {@link StateMachineInterceptor} which can be registered into a {@link StateMachine}
* order to intercept a various security related checks.
*
* @author Janne Valkealahti
*
* @param <S> the type of state
* @param <E> the type of event
*/
public class StateMachineSecurityInterceptor<S, E> extends StateMachineInterceptorAdapter<S, E> {
private AccessDecisionManager transitionAccessDecisionManager;
private AccessDecisionManager eventAccessDecisionManager;
private final ExpressionParser expressionParser = new SpelExpressionParser(new SpelParserConfiguration(SpelCompilerMode.OFF, null));
private SecurityRule eventSecurityRule;
/**
* Instantiates a new state machine security interceptor.
*/
public StateMachineSecurityInterceptor() {
this(null, null);
}
/**
* Instantiates a new state machine security interceptor with
* a custom {@link AccessDecisionManager} for both transitions
* and events.
*
* @param transitionAccessDecisionManager the transition access decision manager
* @param eventAccessDecisionManager the event access decision manager
*/
public StateMachineSecurityInterceptor(AccessDecisionManager transitionAccessDecisionManager, AccessDecisionManager eventAccessDecisionManager) {
this(transitionAccessDecisionManager, eventAccessDecisionManager, null);
}
/**
* Instantiates a new state machine security interceptor with
* a custom {@link AccessDecisionManager} for both transitions
* and events and a {@link SecurityRule} for events;
*
* @param transitionAccessDecisionManager the transition access decision manager
* @param eventAccessDecisionManager the event access decision manager
* @param eventSecurityRule the event security rule
*/
public StateMachineSecurityInterceptor(AccessDecisionManager transitionAccessDecisionManager,
AccessDecisionManager eventAccessDecisionManager, SecurityRule eventSecurityRule) {
this.transitionAccessDecisionManager = transitionAccessDecisionManager;
this.eventAccessDecisionManager = eventAccessDecisionManager;
this.eventSecurityRule = eventSecurityRule;
}
@Override
public Message<E> preEvent(Message<E> message, StateMachine<S, E> stateMachine) {
if (eventSecurityRule != null) {
decide(eventSecurityRule, message);
}
return super.preEvent(message, stateMachine);
}
@Override
public StateContext<S, E> preTransition(StateContext<S, E> stateContext) {
Transition<S, E> transition = stateContext.getTransition();
SecurityRule rule = transition.getSecurityRule();
if (rule != null) {
decide(rule, transition);
}
return super.preTransition(stateContext);
}
/**
* Sets the event access decision manager.
*
* @param eventAccessDecisionManager the new event access decision manager
*/
public void setEventAccessDecisionManager(AccessDecisionManager eventAccessDecisionManager) {
this.eventAccessDecisionManager = eventAccessDecisionManager;
}
/**
* Sets the transition access decision manager.
*
* @param transitionAccessDecisionManager the new transition access decision manager
*/
public void setTransitionAccessDecisionManager(AccessDecisionManager transitionAccessDecisionManager) {
this.transitionAccessDecisionManager = transitionAccessDecisionManager;
}
/**
* Sets the event security rule.
*
* @param eventSecurityRule the new event security rule
*/
public void setEventSecurityRule(SecurityRule eventSecurityRule) {
this.eventSecurityRule = eventSecurityRule;
}
private void decide(SecurityRule rule, Message<E> object) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Collection<ConfigAttribute> configAttributes = getEentConfigAttributes(rule);
if (eventAccessDecisionManager != null) {
decide(eventAccessDecisionManager, authentication, object, configAttributes);
} else {
decide(createDefaultEventManager(rule), authentication, object, configAttributes);
}
}
private void decide(SecurityRule rule, Transition<S, E> object) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Collection<ConfigAttribute> configAttributes = getTransitionConfigAttributes(rule);
if (transitionAccessDecisionManager != null) {
decide(transitionAccessDecisionManager, authentication, object, configAttributes);
} else {
decide(createDefaultTransitionManager(rule), authentication, object, configAttributes);
}
}
private Collection<ConfigAttribute> getTransitionConfigAttributes(SecurityRule rule) {
List<ConfigAttribute> configAttributes = new ArrayList<ConfigAttribute>();
if (rule.getAttributes() != null) {
for (String attribute : rule.getAttributes()) {
configAttributes.add(new SecurityConfig(attribute));
}
}
if (StringUtils.hasText(rule.getExpression())) {
configAttributes.add(new TransitionExpressionConfigAttribute(expressionParser.parseExpression(rule.getExpression())));
}
return configAttributes;
}
private Collection<ConfigAttribute> getEentConfigAttributes(SecurityRule rule) {
List<ConfigAttribute> configAttributes = new ArrayList<ConfigAttribute>();
if (rule.getAttributes() != null) {
for (String attribute : rule.getAttributes()) {
configAttributes.add(new SecurityConfig(attribute));
}
}
if (StringUtils.hasText(rule.getExpression())) {
configAttributes.add(new EventExpressionConfigAttribute(expressionParser.parseExpression(rule.getExpression())));
}
return configAttributes;
}
private void decide(AccessDecisionManager manager, Authentication authentication, Transition<S, E> object,
Collection<ConfigAttribute> configAttributes) {
if (manager.supports(object.getClass())) {
manager.decide(authentication, object, configAttributes);
}
}
private void decide(AccessDecisionManager manager, Authentication authentication, Message<E> object,
Collection<ConfigAttribute> configAttributes) {
if (manager.supports(object.getClass())) {
manager.decide(authentication, object, configAttributes);
}
}
private AbstractAccessDecisionManager createDefaultTransitionManager(SecurityRule rule) {
List<AccessDecisionVoter<? extends Object>> voters = new ArrayList<AccessDecisionVoter<? extends Object>>();
voters.add(new TransitionExpressionVoter());
voters.add(new TransitionVoter<Object, Object>());
voters.add(new RoleVoter());
if (rule.getComparisonType() == SecurityRule.ComparisonType.ANY) {
return new AffirmativeBased(voters);
} else if (rule.getComparisonType() == SecurityRule.ComparisonType.ALL) {
return new UnanimousBased(voters);
} else if (rule.getComparisonType() == SecurityRule.ComparisonType.MAJORITY) {
return new ConsensusBased(voters);
} else {
throw new IllegalStateException("Unknown SecurityRule match type: " + rule.getComparisonType());
}
}
private AbstractAccessDecisionManager createDefaultEventManager(SecurityRule rule) {
List<AccessDecisionVoter<? extends Object>> voters = new ArrayList<AccessDecisionVoter<? extends Object>>();
voters.add(new EventExpressionVoter<Object>());
voters.add(new EventVoter<Object>());
voters.add(new RoleVoter());
if (rule.getComparisonType() == SecurityRule.ComparisonType.ANY) {
return new AffirmativeBased(voters);
} else if (rule.getComparisonType() == SecurityRule.ComparisonType.ALL) {
return new UnanimousBased(voters);
} else if (rule.getComparisonType() == SecurityRule.ComparisonType.MAJORITY) {
return new ConsensusBased(voters);
} else {
throw new IllegalStateException("Unknown SecurityRule match type: " + rule.getComparisonType());
}
}
@Override
public String toString() {
return "StateMachineSecurityInterceptor [transitionAccessDecisionManager=" + transitionAccessDecisionManager
+ ", eventAccessDecisionManager=" + eventAccessDecisionManager + ", eventSecurityRule=" + eventSecurityRule + "]";
}
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.statemachine.security;
import org.springframework.expression.Expression;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.util.Assert;
/**
* Specific {@link ConfigAttribute} for spel expression.
*
* @author Janne Valkealahti
*
*/
@SuppressWarnings("serial")
public class TransitionExpressionConfigAttribute implements ConfigAttribute {
private final Expression authorizeExpression;
/**
* Instantiates a new transition expression config attribute.
*
* @param authorizeExpression the authorize expression
*/
public TransitionExpressionConfigAttribute(Expression authorizeExpression) {
Assert.notNull(authorizeExpression, "Expression must be set");
this.authorizeExpression = authorizeExpression;
}
@Override
public String getAttribute() {
// always null
return null;
}
/**
* Gets the authorize expression.
*
* @return the authorize expression
*/
Expression getAuthorizeExpression() {
return authorizeExpression;
}
@Override
public String toString() {
return authorizeExpression.getExpressionString();
}
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.statemachine.security;
import java.util.Collection;
import org.springframework.expression.EvaluationContext;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.expression.ExpressionUtils;
import org.springframework.security.access.expression.SecurityExpressionHandler;
import org.springframework.security.core.Authentication;
import org.springframework.statemachine.transition.Transition;
/**
* {@link AccessDecisionVoter} evaluating access via security spel expression.
*
* @author Janne Valkealahti
*
*/
public class TransitionExpressionVoter implements AccessDecisionVoter<Transition<?, ?>> {
private final SecurityExpressionHandler<Transition<?, ?>> expressionHandler = new DefaultTransitionSecurityExpressionHandler();
@Override
public boolean supports(ConfigAttribute attribute) {
return attribute instanceof TransitionExpressionConfigAttribute;
}
@Override
public boolean supports(Class<?> clazz) {
return Transition.class.isAssignableFrom(clazz);
}
@Override
public int vote(Authentication authentication, Transition<?, ?> object, Collection<ConfigAttribute> attributes) {
TransitionExpressionConfigAttribute teca = findConfigAttribute(attributes);
if (teca == null) {
return ACCESS_ABSTAIN;
}
EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication, object);
return ExpressionUtils.evaluateAsBoolean(teca.getAuthorizeExpression(), ctx) ? ACCESS_GRANTED : ACCESS_DENIED;
}
private TransitionExpressionConfigAttribute findConfigAttribute(Collection<ConfigAttribute> attributes) {
for (ConfigAttribute attribute : attributes) {
if (attribute instanceof TransitionExpressionConfigAttribute) {
return (TransitionExpressionConfigAttribute) attribute;
}
}
return null;
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.statemachine.security;
import org.springframework.security.access.expression.SecurityExpressionRoot;
import org.springframework.security.core.Authentication;
import org.springframework.statemachine.transition.Transition;
import org.springframework.util.ObjectUtils;
/**
* Root object for security spel evaluation.
*
* @author Janne Valkealahti
*
*/
public class TransitionSecurityExpressionRoot extends SecurityExpressionRoot {
public final Transition<?, ?> transition;
/**
* Instantiates a new transition security expression root.
*
* @param authentication the authentication
* @param transition the transition
*/
public TransitionSecurityExpressionRoot(Authentication authentication, Transition<?, ?> transition) {
super(authentication);
this.transition = transition;
}
public final boolean hasSource(Object source) {
return ObjectUtils.nullSafeEquals(source, transition.getSource().getId());
}
public final boolean hasTarget(Object source) {
return ObjectUtils.nullSafeEquals(source, transition.getTarget().getId());
}
}

View File

@@ -0,0 +1,124 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.statemachine.security;
import java.util.Collection;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.core.Authentication;
import org.springframework.statemachine.transition.Transition;
/**
* Votes if any {@link ConfigAttribute#getAttribute()} starts with a prefix indicating
* that it is a transition source or target. The default prefixes strings are
* <Code>TRANSITION_SOURCE</code> and <Code>TRANSITION_TARGET</code>, but
* those may be overridden to any value. It may also be set to empty, which means that
* essentially any attribute will be voted on. As described further below, the effect
* of an empty prefix may not be quite desirable.
* <p>
* All comparisons and prefixes are case sensitive.
*
* @author Janne Valkealahti
*
* @param <S> the type of state
* @param <E> the type of event
*/
public class TransitionVoter<S, E> implements AccessDecisionVoter<Transition<S, E>> {
private String transitionSourcePrefix = "TRANSITION_SOURCE_";
private String transitionTargetPrefix = "TRANSITION_TARGET_";
@Override
public boolean supports(ConfigAttribute attribute) {
if ((attribute.getAttribute() != null) && (attribute.getAttribute().startsWith(getTransitionSourcePrefix())
|| attribute.getAttribute().startsWith(getTransitionTargetPrefix()))) {
return true;
} else {
return false;
}
}
@Override
public boolean supports(Class<?> clazz) {
return Transition.class.isAssignableFrom(clazz);
}
@Override
public int vote(Authentication authentication, Transition<S, E> transition, Collection<ConfigAttribute> attributes) {
int result = ACCESS_ABSTAIN;
if (authentication == null) {
return result;
}
S source = transition.getSource().getId();
S target = transition.getTarget().getId();
for (ConfigAttribute attribute : attributes) {
if (this.supports(attribute)) {
result = ACCESS_DENIED;
String attr = attribute.getAttribute();
if (attr.startsWith(getTransitionSourcePrefix())
&& attr.equals(getTransitionSourcePrefix() + source.toString())) {
return ACCESS_GRANTED;
} else if (attr.startsWith(getTransitionTargetPrefix())
&& attr.equals(getTransitionTargetPrefix() + target.toString())) {
return ACCESS_GRANTED;
}
}
}
return result;
}
/**
* Gets the transition source prefix.
*
* @return the transition source prefix
*/
public String getTransitionSourcePrefix() {
return transitionSourcePrefix;
}
/**
* Allows the default transition source prefix of <code>TRANSITION_SOURCE_</code> to
* be overridden. May be set to an empty value, although this is usually not desirable.
*
* @param transitionSourcePrefix the new transition source prefix
*/
public void setTransitionSourcePrefix(String transitionSourcePrefix) {
this.transitionSourcePrefix = transitionSourcePrefix;
}
/**
* Gets the transition target prefix.
*
* @return the transition target prefix
*/
public String getTransitionTargetPrefix() {
return transitionTargetPrefix;
}
/**
* Allows the default transition target prefix of <code>TRANSITION_TARGET_</code> to
* be overridden. May be set to an empty value, although this is usually not desirable.
*
* @param transitionTargetPrefix the new transition source prefix
*/
public void setTransitionTargetPrefix(String transitionTargetPrefix) {
this.transitionTargetPrefix = transitionTargetPrefix;
}
}

View File

@@ -17,6 +17,8 @@ package org.springframework.statemachine.state;
import java.util.Collection;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.statemachine.StateContext;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.action.Action;
@@ -32,6 +34,8 @@ import org.springframework.statemachine.region.Region;
*/
public class ObjectState<S, E> extends AbstractSimpleState<S, E> {
private static final Log log = LogFactory.getLog(ObjectState.class);
/**
* Instantiates a new object state.
*
@@ -122,7 +126,11 @@ public class ObjectState<S, E> extends AbstractSimpleState<S, E> {
Collection<? extends Action<S, E>> actions = getExitActions();
if (actions != null) {
for (Action<S, E> action : actions) {
action.execute(context);
try {
action.execute(context);
} catch (Exception e) {
log.error("Action execution resulted error", e);
}
}
}
}
@@ -132,7 +140,11 @@ public class ObjectState<S, E> extends AbstractSimpleState<S, E> {
Collection<? extends Action<S, E>> actions = getEntryActions();
if (actions != null) {
for (Action<S, E> action : actions) {
action.execute(context);
try {
action.execute(context);
} catch (Exception e) {
log.error("Action execution resulted error", e);
}
}
}
}

View File

@@ -195,6 +195,15 @@ public abstract class AbstractStateMachine<S, E> extends StateMachineObjectSuppo
notifyEventNotAccepted(event);
return false;
}
try {
event = getStateMachineInterceptors().preEvent(event, this);
} catch (Exception e) {
log.info("Event " + event + " threw exception in interceptors, not accepting event");
notifyEventNotAccepted(event);
return false;
}
if (isComplete() || !isRunning()) {
notifyEventNotAccepted(event);
return false;

View File

@@ -190,7 +190,15 @@ public class DefaultStateMachineExecutor<S, E> extends LifecycleObjectSupport im
}
StateContext<S, E> stateContext = buildStateContext(queuedMessage, t, relayStateMachine);
stateContext = interceptors.preTransition(stateContext);
try {
stateContext = interceptors.preTransition(stateContext);
} catch (Exception e) {
// currently expect that if exception is
// thrown, this transition will not match.
// i.e. security may throw AccessDeniedException
log.info("Interceptors threw exception", e);
stateContext = null;
}
if (stateContext == null) {
break;
}

View File

@@ -32,6 +32,16 @@ import org.springframework.statemachine.transition.Transition;
*/
public interface StateMachineInterceptor<S, E> {
/**
* Called before message is sent to processing. Throwing exception or
* returning null will skip the message.
*
* @param message the message
* @param stateMachine the state machine
* @return the intercepted message
*/
Message<E> preEvent(Message<E> message, StateMachine<S, E> stateMachine);
/**
* Called prior of a state change. Throwing an exception
* from this method will stop a state change logic.

View File

@@ -31,6 +31,11 @@ import org.springframework.statemachine.transition.Transition;
*/
public class StateMachineInterceptorAdapter<S, E> implements StateMachineInterceptor<S, E> {
@Override
public Message<E> preEvent(Message<E> message, StateMachine<S, E> stateMachine) {
return message;
}
@Override
public void preStateChange(State<S, E> state, Message<E> message, Transition<S, E> transition,
StateMachine<S, E> stateMachine) {

View File

@@ -71,6 +71,22 @@ public class StateMachineInterceptorList<S, E> {
return interceptors.remove(interceptor);
}
/**
* Pre event.
*
* @param message the message
* @param stateMachine the state machine
* @return the message
*/
public Message<E> preEvent(Message<E> message, StateMachine<S, E> stateMachine) {
for (StateMachineInterceptor<S, E> interceptor : interceptors) {
if ((message = interceptor.preEvent(message, stateMachine)) == null) {
break;
}
}
return message;
}
/**
* Pre state change.
*

View File

@@ -19,14 +19,20 @@ import java.util.Collection;
import org.springframework.statemachine.action.Action;
import org.springframework.statemachine.guard.Guard;
import org.springframework.statemachine.security.SecurityRule;
import org.springframework.statemachine.state.State;
import org.springframework.statemachine.trigger.Trigger;
public abstract class AbstractExternalTransition<S, E> extends AbstractTransition<S, E> implements Transition<S, E> {
public AbstractExternalTransition(State<S,E> source, State<S,E> target, Collection<Action<S, E>> actions, E event, Guard<S, E> guard, Trigger<S, E> trigger) {
public AbstractExternalTransition(State<S, E> source, State<S, E> target, Collection<Action<S, E>> actions,
E event, Guard<S, E> guard, Trigger<S, E> trigger, SecurityRule securityRule) {
super(source, target, actions, event, TransitionKind.EXTERNAL, guard, trigger, securityRule);
}
public AbstractExternalTransition(State<S, E> source, State<S, E> target, Collection<Action<S, E>> actions,
E event, Guard<S, E> guard, Trigger<S, E> trigger) {
super(source, target, actions, event, TransitionKind.EXTERNAL, guard, trigger);
}
}

View File

@@ -19,13 +19,20 @@ import java.util.Collection;
import org.springframework.statemachine.action.Action;
import org.springframework.statemachine.guard.Guard;
import org.springframework.statemachine.security.SecurityRule;
import org.springframework.statemachine.state.State;
import org.springframework.statemachine.trigger.Trigger;
public class AbstractInternalTransition <S, E> extends AbstractTransition<S, E> implements Transition<S, E> {
public class AbstractInternalTransition<S, E> extends AbstractTransition<S, E> implements Transition<S, E> {
public AbstractInternalTransition(State<S, E> source,Collection<Action<S, E>> actions, E event, Guard<S, E> guard, Trigger<S, E> trigger) {
public AbstractInternalTransition(State<S, E> source, Collection<Action<S, E>> actions, E event, Guard<S, E> guard,
Trigger<S, E> trigger) {
super(source, source, actions, event, TransitionKind.INTERNAL, guard, trigger);
}
public AbstractInternalTransition(State<S, E> source, Collection<Action<S, E>> actions, E event, Guard<S, E> guard,
Trigger<S, E> trigger, SecurityRule securityRule) {
super(source, source, actions, event, TransitionKind.INTERNAL, guard, trigger, securityRule);
}
}

View File

@@ -19,13 +19,20 @@ import java.util.Collection;
import org.springframework.statemachine.action.Action;
import org.springframework.statemachine.guard.Guard;
import org.springframework.statemachine.security.SecurityRule;
import org.springframework.statemachine.state.State;
import org.springframework.statemachine.trigger.Trigger;
public class AbstractLocalTransition <S, E> extends AbstractTransition<S, E> implements Transition<S, E> {
public class AbstractLocalTransition<S, E> extends AbstractTransition<S, E> implements Transition<S, E> {
public AbstractLocalTransition(State<S, E> source, State<S,E> target,Collection<Action<S, E>> actions, E event, Guard<S, E> guard, Trigger<S, E> trigger) {
public AbstractLocalTransition(State<S, E> source, State<S, E> target, Collection<Action<S, E>> actions, E event,
Guard<S, E> guard, Trigger<S, E> trigger) {
super(source, target, actions, event, TransitionKind.LOCAL, guard, trigger);
}
public AbstractLocalTransition(State<S, E> source, State<S, E> target, Collection<Action<S, E>> actions, E event,
Guard<S, E> guard, Trigger<S, E> trigger, SecurityRule securityRule) {
super(source, target, actions, event, TransitionKind.LOCAL, guard, trigger, securityRule);
}
}

View File

@@ -20,6 +20,7 @@ import java.util.Collection;
import org.springframework.statemachine.StateContext;
import org.springframework.statemachine.action.Action;
import org.springframework.statemachine.guard.Guard;
import org.springframework.statemachine.security.SecurityRule;
import org.springframework.statemachine.state.State;
import org.springframework.statemachine.trigger.Trigger;
import org.springframework.util.Assert;
@@ -44,10 +45,17 @@ public abstract class AbstractTransition<S, E> implements Transition<S, E> {
private final Guard<S, E> guard;
private Trigger<S, E> trigger;
private final Trigger<S, E> trigger;
private final SecurityRule securityRule;
public AbstractTransition(State<S, E> source, State<S, E> target, Collection<Action<S, E>> actions, E event,
TransitionKind kind, Guard<S, E> guard, Trigger<S, E> trigger) {
this(source, target, actions, event, kind, guard, trigger, null);
}
public AbstractTransition(State<S, E> source, State<S, E> target, Collection<Action<S, E>> actions, E event,
TransitionKind kind, Guard<S, E> guard, Trigger<S, E> trigger, SecurityRule securityRule) {
Assert.notNull(source, "Source must be set");
Assert.notNull(kind, "Transition type must be set");
this.source = source;
@@ -56,6 +64,7 @@ public abstract class AbstractTransition<S, E> implements Transition<S, E> {
this.kind = kind;
this.guard = guard;
this.trigger = trigger;
this.securityRule = securityRule;
}
@Override
@@ -98,4 +107,9 @@ public abstract class AbstractTransition<S, E> implements Transition<S, E> {
return kind;
}
@Override
public SecurityRule getSecurityRule() {
return securityRule;
}
}

View File

@@ -19,13 +19,20 @@ import java.util.Collection;
import org.springframework.statemachine.action.Action;
import org.springframework.statemachine.guard.Guard;
import org.springframework.statemachine.security.SecurityRule;
import org.springframework.statemachine.state.State;
import org.springframework.statemachine.trigger.Trigger;
public class DefaultExternalTransition<S, E> extends AbstractExternalTransition<S, E> {
public DefaultExternalTransition(State<S,E> source, State<S,E> target, Collection<Action<S, E>> actions, E event, Guard<S, E> guard, Trigger<S, E> trigger) {
public DefaultExternalTransition(State<S, E> source, State<S, E> target, Collection<Action<S, E>> actions, E event,
Guard<S, E> guard, Trigger<S, E> trigger) {
super(source, target, actions, event, guard, trigger);
}
public DefaultExternalTransition(State<S, E> source, State<S, E> target, Collection<Action<S, E>> actions, E event,
Guard<S, E> guard, Trigger<S, E> trigger, SecurityRule securityRule) {
super(source, target, actions, event, guard, trigger, securityRule);
}
}

View File

@@ -19,13 +19,20 @@ import java.util.Collection;
import org.springframework.statemachine.action.Action;
import org.springframework.statemachine.guard.Guard;
import org.springframework.statemachine.security.SecurityRule;
import org.springframework.statemachine.state.State;
import org.springframework.statemachine.trigger.Trigger;
public class DefaultInternalTransition<S, E> extends AbstractInternalTransition<S, E> {
public DefaultInternalTransition(State<S, E> source, Collection<Action<S, E>> actions, E event, Guard<S, E> guard, Trigger<S, E> trigger) {
public DefaultInternalTransition(State<S, E> source, Collection<Action<S, E>> actions, E event, Guard<S, E> guard,
Trigger<S, E> trigger) {
super(source, actions, event, guard, trigger);
}
public DefaultInternalTransition(State<S, E> source, Collection<Action<S, E>> actions, E event, Guard<S, E> guard,
Trigger<S, E> trigger, SecurityRule securityRule) {
super(source, actions, event, guard, trigger, securityRule);
}
}

View File

@@ -20,6 +20,7 @@ import java.util.Collection;
import org.springframework.statemachine.StateContext;
import org.springframework.statemachine.action.Action;
import org.springframework.statemachine.security.SecurityRule;
import org.springframework.statemachine.state.State;
import org.springframework.statemachine.trigger.Trigger;
@@ -108,4 +109,10 @@ public class InitialTransition<S, E> implements Transition<S, E> {
return TransitionKind.INITIAL;
}
@Override
public SecurityRule getSecurityRule() {
// initial cannot have security
return null;
}
}

View File

@@ -19,6 +19,7 @@ import java.util.Collection;
import org.springframework.statemachine.StateContext;
import org.springframework.statemachine.action.Action;
import org.springframework.statemachine.security.SecurityRule;
import org.springframework.statemachine.state.State;
import org.springframework.statemachine.trigger.Trigger;
@@ -27,7 +28,7 @@ import org.springframework.statemachine.trigger.Trigger;
* changes.
*
* @author Janne Valkealahti
*
*
* @param <S> the type of state
* @param <E> the type of event
*/
@@ -40,7 +41,7 @@ public interface Transition<S, E> {
* @return true, if transition happened, false otherwise
*/
boolean transit(StateContext<S, E> context);
/**
* Gets the source state of this transition.
*
@@ -76,4 +77,11 @@ public interface Transition<S, E> {
*/
TransitionKind getKind();
/**
* Gets the security rule.
*
* @return the security rule
*/
SecurityRule getSecurityRule();
}

View File

@@ -956,6 +956,11 @@ public class DocsConfigurationSampleTests extends AbstractStateMachineTests {
stateMachine.getStateMachineAccessor()
.withRegion().addStateMachineInterceptor(new StateMachineInterceptor<String, String>() {
@Override
public Message<String> preEvent(Message<String> message, StateMachine<String, String> stateMachine) {
return message;
}
@Override
public StateContext<String, String> preTransition(StateContext<String, String> stateContext) {
return stateContext;
@@ -1021,7 +1026,7 @@ public class DocsConfigurationSampleTests extends AbstractStateMachineTests {
@Override
public void stateMachineError(StateMachine<String, String> stateMachine, Exception exception) {
// do something with error
// do something with error
}
}
// end::snippet2[]
@@ -1030,12 +1035,12 @@ public class DocsConfigurationSampleTests extends AbstractStateMachineTests {
public class GenericApplicationEventListener
implements ApplicationListener<StateMachineEvent> {
@Override
public void onApplicationEvent(StateMachineEvent event) {
if (event instanceof OnStateMachineError) {
// do something with error
}
}
@Override
public void onApplicationEvent(StateMachineEvent event) {
if (event instanceof OnStateMachineError) {
// do something with error
}
}
}
// end::snippet3[]
@@ -1045,7 +1050,7 @@ public class DocsConfigurationSampleTests extends AbstractStateMachineTests {
@Override
public void onApplicationEvent(OnStateMachineError event) {
// do something with error
// do something with error
}
}
// end::snippet4[]

View File

@@ -0,0 +1,171 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.statemachine.docs;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.statemachine.StateContext;
import org.springframework.statemachine.action.Action;
import org.springframework.statemachine.config.EnableStateMachine;
import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
import org.springframework.statemachine.security.SecurityRule.ComparisonType;
public class DocsConfigurationSampleTests3 {
// tag::snippetA[]
@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);
}
}
// end::snippetA[]
// tag::snippetB[]
@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')");
}
}
// end::snippetB[]
// tag::snippetC[]
@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) {
}
};
}
}
// end::snippetC[]
// tag::snippetD[]
@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);
}
}
// end::snippetD[]
// tag::snippetE[]
@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");
}
}
// end::snippetE[]
// tag::snippetF[]
@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);
}
}
// end::snippetF[]
}

View File

@@ -0,0 +1,136 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.statemachine.security;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.springframework.core.task.SyncTaskExecutor;
import org.springframework.statemachine.AbstractStateMachineTests;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.config.StateMachineBuilder;
import org.springframework.statemachine.config.StateMachineBuilder.Builder;
import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer;
import org.springframework.statemachine.config.configurers.ExternalTransitionConfigurer;
import org.springframework.statemachine.config.configurers.SecurityConfigurer;
import org.springframework.statemachine.listener.StateMachineListenerAdapter;
import org.springframework.statemachine.security.SecurityRule.ComparisonType;
import org.springframework.statemachine.state.State;
public abstract class AbstractSecurityTests extends AbstractStateMachineTests {
protected static void assertTransitionAllowed(StateMachine<States, Events> machine, TestListener listener) throws Exception {
assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true));
assertThat(listener.stateChangedCount, is(1));
assertThat(machine.getState().getIds(), containsInAnyOrder(States.S0));
listener.reset(1);
machine.sendEvent(Events.A);
assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true));
assertThat(listener.stateChangedCount, is(1));
assertThat(machine.getState().getIds(), containsInAnyOrder(States.S1));
}
protected static void assertTransitionDenied(StateMachine<States, Events> machine, TestListener listener) throws Exception {
assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true));
assertThat(listener.stateChangedCount, is(1));
assertThat(machine.getState().getIds(), containsInAnyOrder(States.S0));
listener.reset(1);
machine.sendEvent(Events.A);
assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(false));
assertThat(listener.stateChangedCount, is(0));
assertThat(machine.getState().getIds(), containsInAnyOrder(States.S0));
}
protected static enum States {
S0, S1;
}
protected static enum Events {
A;
}
protected static class TestListener extends StateMachineListenerAdapter<States, Events> {
volatile CountDownLatch stateChangedLatch = new CountDownLatch(1);
volatile int stateChangedCount = 0;
@Override
public void stateChanged(State<States, Events> from, State<States, Events> to) {
stateChangedCount++;
stateChangedLatch.countDown();
}
public void reset(int c1) {
stateChangedLatch = new CountDownLatch(c1);
stateChangedCount = 0;
}
}
protected static StateMachine<States, Events> buildMachine(TestListener listener, String attributes, ComparisonType match, String expression) throws Exception {
return buildMachine(listener, attributes, match, expression, null, null, null);
}
protected static StateMachine<States, Events> buildMachine(TestListener listener, String attributes, ComparisonType match,
String expression, String eventAttributes, ComparisonType eventMatch, String eventExpression) throws Exception {
Builder<States, Events> builder = StateMachineBuilder.<States, Events>builder();
StateMachineConfigurationConfigurer<States, Events> configureConfiguration = builder.configureConfiguration();
configureConfiguration.withConfiguration()
.listener(listener)
.autoStartup(true)
.taskExecutor(new SyncTaskExecutor());
SecurityConfigurer<States, Events> withSecurity = configureConfiguration.withSecurity();
withSecurity.enabled(true);
if (eventAttributes != null && eventMatch != null) {
withSecurity.event(eventAttributes, eventMatch);
}
if (eventExpression != null) {
withSecurity.event(eventExpression);
}
builder.configureStates()
.withStates()
.initial(States.S0)
.state(States.S0)
.state(States.S1);
ExternalTransitionConfigurer<States, Events> withExternal = builder.configureTransitions()
.withExternal();
if (attributes != null) {
withExternal.secured(attributes, match);
}
if (expression != null) {
withExternal.secured(expression);
}
withExternal.source(States.S0);
withExternal.target(States.S1);
withExternal.event(Events.A);
return builder.build();
}
}

View File

@@ -0,0 +1,208 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.statemachine.security;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.statemachine.AbstractStateMachineTests;
import org.springframework.statemachine.StateContext;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.action.Action;
import org.springframework.statemachine.config.EnableStateMachine;
import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter;
import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
import org.springframework.statemachine.listener.StateMachineListenerAdapter;
import org.springframework.statemachine.security.ActionSecurityTests.Config1;
import org.springframework.statemachine.security.ActionSecurityTests.Config2;
import org.springframework.statemachine.state.State;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.annotation.DirtiesContext.ClassMode;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* Tests for securing actions.
*
* @author Janne Valkealahti
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {Config1.class, Config2.class})
@DirtiesContext(classMode = ClassMode.BEFORE_EACH_TEST_METHOD)
public class ActionSecurityTests extends AbstractStateMachineTests {
@Autowired
StateMachine<States, Events> machine;
@Autowired
TestListener listener;
@Autowired
TestSecAction action1;
@Test
@WithMockUser(roles = { "FOO" })
public void testActionExecutionDenied() throws Exception {
assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true));
assertThat(listener.stateChangedCount, is(1));
assertThat(machine.getState().getIds(), containsInAnyOrder(States.S0));
listener.reset(1);
machine.sendEvent(Events.A);
assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true));
assertThat(listener.stateChangedCount, is(1));
assertThat(machine.getState().getIds(), containsInAnyOrder(States.S1));
assertThat(action1.getCount(), is(0));
}
@Test
@WithMockUser
public void testActionExecutionAllowed() throws Exception {
assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true));
assertThat(listener.stateChangedCount, is(1));
assertThat(machine.getState().getIds(), containsInAnyOrder(States.S0));
listener.reset(1);
machine.sendEvent(Events.A);
assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true));
assertThat(listener.stateChangedCount, is(1));
assertThat(machine.getState().getIds(), containsInAnyOrder(States.S1));
assertThat(action1.getCount(), is(1));
}
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true)
public static class Config1 extends WebSecurityConfigurerAdapter {
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("user").password("password").roles("USER");
}
}
@Configuration
@EnableStateMachine
public static class Config2 extends EnumStateMachineConfigurerAdapter<States, Events> {
@Override
public void configure(StateMachineConfigurationConfigurer<States, Events> config)
throws Exception {
config
.withConfiguration()
.listener(testListener())
.autoStartup(true)
.and()
.withSecurity()
.enabled(true);
}
@Override
public void configure(StateMachineStateConfigurer<States, Events> states)
throws Exception {
states
.withStates()
.initial(States.S0)
.state(States.S0)
.state(States.S1, action1(), null);
}
@Override
public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
throws Exception {
transitions
.withExternal()
.source(States.S0)
.target(States.S1)
.event(Events.A);
}
@Bean
public TestListener testListener() {
return new TestListener();
}
@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)
@Bean
public TestSecAction action1() {
return new TestSecAction();
}
}
public static enum States {
S0, S1;
}
public static enum Events {
A;
}
private static class TestSecAction implements Action<States, Events> {
int count;
@Secured("ROLE_USER")
@Override
public void execute(StateContext<States, Events> context) {
count++;
}
public int getCount() {
return count;
}
}
private static class TestListener extends StateMachineListenerAdapter<States, Events> {
volatile CountDownLatch stateChangedLatch = new CountDownLatch(1);
volatile int stateChangedCount = 0;
@Override
public void stateChanged(State<States, Events> from, State<States, Events> to) {
stateChangedCount++;
stateChangedLatch.countDown();
}
public void reset(int c1) {
stateChangedLatch = new CountDownLatch(c1);
stateChangedCount = 0;
}
}
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.statemachine.security;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.security.SecurityRule.ComparisonType;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.annotation.DirtiesContext.ClassMode;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@DirtiesContext(classMode = ClassMode.BEFORE_EACH_TEST_METHOD)
public class EventSecurityTests extends AbstractSecurityTests {
@Test
public void testNoSecurityContext() throws Exception {
TestListener listener = new TestListener();
StateMachine<States,Events> machine = buildMachine(listener, "ROLE_ANONYMOUS", ComparisonType.ANY, null);
assertTransitionDenied(machine, listener);
}
@Test
@WithMockUser(roles = { "ANONYMOUS" })
public void testEventDeniedViaExpression() throws Exception {
TestListener listener = new TestListener();
StateMachine<States,Events> machine = buildMachine(listener, null, null, null, null, null, "false");
assertTransitionDenied(machine, listener);
}
@Test
@WithMockUser(roles = { "ANONYMOUS" })
public void testEventDeniedViaAttributes() throws Exception {
TestListener listener = new TestListener();
StateMachine<States,Events> machine = buildMachine(listener, null, null, null, "EVENT_B", ComparisonType.ALL, null);
assertTransitionDenied(machine, listener);
}
@Test
@WithMockUser(roles = { "ANONYMOUS" })
public void testEventAllowedViaAttributes() throws Exception {
TestListener listener = new TestListener();
StateMachine<States,Events> machine = buildMachine(listener, null, null, null, "EVENT_A", ComparisonType.ALL, null);
assertTransitionAllowed(machine, listener);
}
@Configuration
public static class Config {
}
}

View File

@@ -0,0 +1,479 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.statemachine.security;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import java.util.Collection;
import java.util.List;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.statemachine.AbstractStateMachineTests;
import org.springframework.statemachine.ObjectStateMachine;
import org.springframework.statemachine.StateMachineSystemConstants;
import org.springframework.statemachine.TestUtils;
import org.springframework.statemachine.config.EnableStateMachine;
import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
import org.springframework.statemachine.security.SecurityRule.ComparisonType;
import org.springframework.statemachine.support.StateMachineInterceptor;
import org.springframework.statemachine.support.StateMachineInterceptorList;
import org.springframework.statemachine.transition.Transition;
/**
* Generic security config tests.
*
* @author Janne Valkealahti
*
*/
public class SecurityConfigTests extends AbstractStateMachineTests {
@Override
protected AnnotationConfigApplicationContext buildContext() {
return new AnnotationConfigApplicationContext();
}
@Test
public void testSecurityEnabledWithTrue() throws Exception {
context.register(Config1.class);
context.refresh();
assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE));
@SuppressWarnings("unchecked")
ObjectStateMachine<String, String> machine =
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class);
assertThat(machine, notNullValue());
StateMachineInterceptorList<?, ?> ilist = TestUtils.readField("interceptors", machine);
List<StateMachineInterceptor<?, ?>> interceptors = TestUtils.readField("interceptors", ilist);
assertThat(interceptors, notNullValue());
assertThat(interceptors.size(), is(1));
assertThat(interceptors.get(0), instanceOf(StateMachineSecurityInterceptor.class));
Object adm = TestUtils.readField("transitionAccessDecisionManager", interceptors.get(0));
assertThat(adm, nullValue());
}
@Test
public void testSecurityDisabledWithFalse() throws Exception {
context.register(Config2.class);
context.refresh();
assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE));
@SuppressWarnings("unchecked")
ObjectStateMachine<String, String> machine =
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class);
assertThat(machine, notNullValue());
StateMachineInterceptorList<?, ?> ilist = TestUtils.readField("interceptors", machine);
List<StateMachineInterceptor<?, ?>> interceptors = TestUtils.readField("interceptors", ilist);
assertThat(interceptors, notNullValue());
assertThat(interceptors.size(), is(0));
}
@Test
public void testSecurityEnabledWithJustWith() throws Exception {
context.register(Config3.class);
context.refresh();
assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE));
@SuppressWarnings("unchecked")
ObjectStateMachine<String, String> machine =
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class);
assertThat(machine, notNullValue());
StateMachineInterceptorList<?, ?> ilist = TestUtils.readField("interceptors", machine);
List<StateMachineInterceptor<?, ?>> interceptors = TestUtils.readField("interceptors", ilist);
assertThat(interceptors.size(), is(1));
assertThat(interceptors.get(0), instanceOf(StateMachineSecurityInterceptor.class));
Object adm = TestUtils.readField("transitionAccessDecisionManager", interceptors.get(0));
assertThat(adm, nullValue());
}
@Test
public void testSecurityDisabledNoSecurityConfigurer() throws Exception {
context.register(Config4.class);
context.refresh();
assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE));
@SuppressWarnings("unchecked")
ObjectStateMachine<String, String> machine =
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class);
assertThat(machine, notNullValue());
StateMachineInterceptorList<?, ?> ilist = TestUtils.readField("interceptors", machine);
List<StateMachineInterceptor<?, ?>> interceptors = TestUtils.readField("interceptors", ilist);
assertThat(interceptors, notNullValue());
assertThat(interceptors.size(), is(0));
}
@Test
public void testCustomAccessDecisionManager() throws Exception {
context.register(Config5.class);
context.refresh();
assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE));
@SuppressWarnings("unchecked")
ObjectStateMachine<String, String> machine =
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class);
assertThat(machine, notNullValue());
StateMachineInterceptorList<?, ?> ilist = TestUtils.readField("interceptors", machine);
List<StateMachineInterceptor<?, ?>> interceptors = TestUtils.readField("interceptors", ilist);
assertThat(interceptors, notNullValue());
assertThat(interceptors.size(), is(1));
assertThat(interceptors.get(0), instanceOf(StateMachineSecurityInterceptor.class));
Object adm = TestUtils.readField("transitionAccessDecisionManager", interceptors.get(0));
assertThat(adm, notNullValue());
assertThat(adm, instanceOf(MockAccessDecisionManager.class));
}
@Test
public void testTransitionExplicit() throws Exception {
context.register(Config6.class);
context.refresh();
assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE));
@SuppressWarnings("unchecked")
ObjectStateMachine<String, String> machine =
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class);
assertThat(machine, notNullValue());
Transition<String, String> transition = machine.getTransitions().iterator().next();
assertThat(transition.getSecurityRule(), notNullValue());
}
@Test
public void testTransitionGlobal() throws Exception {
context.register(Config8.class);
context.refresh();
assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE));
@SuppressWarnings("unchecked")
ObjectStateMachine<String, String> machine =
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class);
assertThat(machine, notNullValue());
Transition<String, String> transition = machine.getTransitions().iterator().next();
assertThat(transition.getSecurityRule(), notNullValue());
}
@Test
public void testEventRule() throws Exception {
context.register(Config7.class);
context.refresh();
assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE));
@SuppressWarnings("unchecked")
ObjectStateMachine<String, String> machine =
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class);
assertThat(machine, notNullValue());
StateMachineInterceptorList<?, ?> ilist = TestUtils.readField("interceptors", machine);
List<StateMachineInterceptor<?, ?>> interceptors = TestUtils.readField("interceptors", ilist);
assertThat(interceptors, notNullValue());
assertThat(interceptors.size(), is(1));
assertThat(interceptors.get(0), instanceOf(StateMachineSecurityInterceptor.class));
Object adm = TestUtils.readField("eventSecurityRule", interceptors.get(0));
assertThat(adm, notNullValue());
}
@Configuration
@EnableStateMachine
static class Config1 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")
.event("A");
}
}
@Configuration
@EnableStateMachine
static class Config2 extends StateMachineConfigurerAdapter<String, String> {
@Override
public void configure(StateMachineConfigurationConfigurer<String, String> config)
throws Exception {
config
.withSecurity()
.enabled(false);
}
@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")
.event("A");
}
}
@Configuration
@EnableStateMachine
static class Config3 extends StateMachineConfigurerAdapter<String, String> {
@Override
public void configure(StateMachineConfigurationConfigurer<String, String> config)
throws Exception {
config
.withSecurity();
}
@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")
.event("A");
}
}
@Configuration
@EnableStateMachine
static class Config4 extends StateMachineConfigurerAdapter<String, String> {
@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")
.event("A");
}
}
@Configuration
@EnableStateMachine
static class Config5 extends StateMachineConfigurerAdapter<String, String> {
@Override
public void configure(StateMachineConfigurationConfigurer<String, String> config)
throws Exception {
config
.withSecurity()
.eventAccessDecisionManager(new MockAccessDecisionManager())
.transitionAccessDecisionManager(new MockAccessDecisionManager());
}
@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")
.event("A");
}
}
@Configuration
@EnableStateMachine
static class Config6 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")
.event("A")
.secured("expression")
.secured("FOO", ComparisonType.ALL);
}
}
@Configuration
@EnableStateMachine
static class Config7 extends StateMachineConfigurerAdapter<String, String> {
@Override
public void configure(StateMachineConfigurationConfigurer<String, String> config)
throws Exception {
config
.withSecurity()
.enabled(true)
.event("expression")
.event("FOO", ComparisonType.ALL);
}
@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")
.event("A");
}
}
@Configuration
@EnableStateMachine
static class Config8 extends StateMachineConfigurerAdapter<String, String> {
@Override
public void configure(StateMachineConfigurationConfigurer<String, String> config)
throws Exception {
config
.withSecurity()
.enabled(true)
.transition("expression")
.transition("FOO", ComparisonType.ALL);
}
@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")
.event("A");
}
}
private static class MockAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
throws AccessDeniedException, InsufficientAuthenticationException {
}
@Override
public boolean supports(ConfigAttribute attribute) {
return false;
}
@Override
public boolean supports(Class<?> clazz) {
return false;
}
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.statemachine.security;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.util.ArrayList;
import java.util.Collection;
import org.junit.Test;
public class SecurityRuleTests {
@Test
public void testConvertAttributesToCommaSeparatedString() {
Collection<String> attributes = new ArrayList<String>();
attributes.add("ROLE_1");
attributes.add("ROLE_2");
assertEquals("ROLE_1, ROLE_2", SecurityRule.securityAttributesToCommaDelimitedList(attributes));
}
@Test
public void testConvertAttributesFromCommaSeparatedString() {
Collection<String> attributes = SecurityRule.commaDelimitedListToSecurityAttributes(" ,,ROLE_1, ROLE_2");
assertEquals(2, attributes.size());
assertTrue(attributes.contains("ROLE_1"));
assertTrue(attributes.contains("ROLE_2"));
}
@Test
public void testDefaultComparisonType() {
SecurityRule rule = new SecurityRule();
assertTrue(rule.getComparisonType() == SecurityRule.ComparisonType.ANY);
}
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.statemachine.security;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.security.SecurityRule.ComparisonType;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.annotation.DirtiesContext.ClassMode;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@DirtiesContext(classMode = ClassMode.BEFORE_EACH_TEST_METHOD)
public class TransitionSecurityAttributeTests extends AbstractSecurityTests {
@Test
@WithMockUser
public void testAnonymousRole() throws Exception {
TestListener listener = new TestListener();
StateMachine<States,Events> machine = buildMachine(listener, "ROLE_ANONYMOUS", ComparisonType.ANY, null);
assertTransitionDenied(machine, listener);
}
@Test
@WithMockUser
public void testTransitionSource() throws Exception {
TestListener listener = new TestListener();
StateMachine<States,Events> machine = buildMachine(listener, "TRANSITION_SOURCE_S0", ComparisonType.ANY, null);
assertTransitionAllowed(machine, listener);
}
@Test
@WithMockUser
public void testTransitionTarget() throws Exception {
TestListener listener = new TestListener();
StateMachine<States,Events> machine = buildMachine(listener, "TRANSITION_TARGET_S1", ComparisonType.ANY, null);
assertTransitionAllowed(machine, listener);
}
@Test
@WithMockUser
public void testTransitionSourceAndTargetAll() throws Exception {
TestListener listener = new TestListener();
StateMachine<States,Events> machine = buildMachine(listener, "TRANSITION_SOURCE_S0,TRANSITION_TARGET_S1", ComparisonType.ALL, null);
assertTransitionAllowed(machine, listener);
}
@Test
@WithMockUser
public void testTransitionSourceAndTargetAny() throws Exception {
TestListener listener = new TestListener();
StateMachine<States,Events> machine = buildMachine(listener, "TRANSITION_SOURCE_S0,TRANSITION_TARGET_S1", ComparisonType.ANY, null);
assertTransitionAllowed(machine, listener);
}
@Configuration
public static class Config {
}
}

View File

@@ -0,0 +1,133 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.statemachine.security;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import org.junit.Before;
import org.junit.Test;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.access.expression.ExpressionUtils;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.core.Authentication;
import org.springframework.statemachine.state.State;
import org.springframework.statemachine.transition.Transition;
public class TransitionSecurityExpressionRootTests {
SpelExpressionParser parser = new SpelExpressionParser();
TransitionSecurityExpressionRoot root;
StandardEvaluationContext ctx;
private AuthenticationTrustResolver trustResolver;
private Authentication user;
private Transition<?, ?> transition;
@Before
public void createContext() {
user = mock(Authentication.class);
transition = mock(Transition.class);
root = new TransitionSecurityExpressionRoot(user, transition);
ctx = new StandardEvaluationContext();
ctx.setRootObject(root);
trustResolver = mock(AuthenticationTrustResolver.class);
root.setTrustResolver(trustResolver);
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@Test
public void testSourceTarget() throws Exception {
State source = mock(State.class);
when(source.getId()).thenReturn("S1");
State target = mock(State.class);
when(target.getId()).thenReturn("S2");
when(transition.getSource()).thenReturn(source);
when(transition.getTarget()).thenReturn(target);
Expression e1 = parser.parseExpression("hasSource('S1')");
assertTrue(ExpressionUtils.evaluateAsBoolean(e1, ctx));
Expression e2 = parser.parseExpression("hasTarget('S2')");
assertTrue(ExpressionUtils.evaluateAsBoolean(e2, ctx));
}
@Test
public void canCallMethodsOnVariables() throws Exception {
ctx.setVariable("var", "somestring");
Expression e = parser.parseExpression("#var.length() == 10");
assertTrue(ExpressionUtils.evaluateAsBoolean(e, ctx));
}
@Test
public void isAnonymousReturnsTrueIfTrustResolverReportsAnonymous() {
when(trustResolver.isAnonymous(user)).thenReturn(true);
assertTrue(root.isAnonymous());
}
@Test
public void isAnonymousReturnsFalseIfTrustResolverReportsNonAnonymous() {
when(trustResolver.isAnonymous(user)).thenReturn(false);
assertFalse(root.isAnonymous());
}
@Test
public void hasPermissionOnDomainObjectReturnsFalseIfPermissionEvaluatorDoes() throws Exception {
final Object dummyDomainObject = new Object();
final PermissionEvaluator pe = mock(PermissionEvaluator.class);
ctx.setVariable("domainObject", dummyDomainObject);
root.setPermissionEvaluator(pe);
when(pe.hasPermission(user, dummyDomainObject, "ignored")).thenReturn(false);
assertFalse(root.hasPermission(dummyDomainObject, "ignored"));
}
@Test
public void hasPermissionOnDomainObjectReturnsTrueIfPermissionEvaluatorDoes() throws Exception {
final Object dummyDomainObject = new Object();
final PermissionEvaluator pe = mock(PermissionEvaluator.class);
ctx.setVariable("domainObject", dummyDomainObject);
root.setPermissionEvaluator(pe);
when(pe.hasPermission(user, dummyDomainObject, "ignored")).thenReturn(true);
assertTrue(root.hasPermission(dummyDomainObject, "ignored"));
}
@Test
public void hasPermissionOnDomainObjectWorksWithIntegerExpressions() throws Exception {
final Object dummyDomainObject = new Object();
ctx.setVariable("domainObject", dummyDomainObject);
final PermissionEvaluator pe = mock(PermissionEvaluator.class);
root.setPermissionEvaluator(pe);
when(pe.hasPermission(eq(user), eq(dummyDomainObject), any(Integer.class))).thenReturn(true).thenReturn(true).thenReturn(false);
Expression e = parser.parseExpression("hasPermission(#domainObject, 0xA)");
// evaluator returns true
assertTrue(ExpressionUtils.evaluateAsBoolean(e, ctx));
e = parser.parseExpression("hasPermission(#domainObject, 10)");
// evaluator returns true
assertTrue(ExpressionUtils.evaluateAsBoolean(e, ctx));
e = parser.parseExpression("hasPermission(#domainObject, 0xFF)");
// evaluator returns false, make sure return value matches
assertFalse(ExpressionUtils.evaluateAsBoolean(e, ctx));
}
}

View File

@@ -0,0 +1,156 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.statemachine.security;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.SyncTaskExecutor;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.statemachine.AbstractStateMachineTests;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.config.StateMachineBuilder;
import org.springframework.statemachine.config.StateMachineBuilder.Builder;
import org.springframework.statemachine.config.configurers.ExternalTransitionConfigurer;
import org.springframework.statemachine.listener.StateMachineListenerAdapter;
import org.springframework.statemachine.security.SecurityRule.ComparisonType;
import org.springframework.statemachine.state.State;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.annotation.DirtiesContext.ClassMode;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* Tests for securing transitions.
*
* @author Janne Valkealahti
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@DirtiesContext(classMode = ClassMode.BEFORE_EACH_TEST_METHOD)
public class TransitionSecurityExpressionTests extends AbstractStateMachineTests {
@Test
@WithMockUser(authorities = { "FOO" })
public void testAttr() throws Exception {
TestListener listener = new TestListener();
StateMachine<States,Events> machine = buildMachine(listener, "ROLE_ANONYMOUS", ComparisonType.ANY, null);
assertTransitionDenied(machine, listener);
}
@Test
@WithMockUser(roles = { "FOO" })
public void testExpression() throws Exception {
TestListener listener = new TestListener();
StateMachine<States,Events> machine = buildMachine(listener, null, null, "hasRole('FOO')");
assertTransitionAllowed(machine, listener);
}
private static void assertTransitionAllowed(StateMachine<States, Events> machine, TestListener listener) throws Exception {
assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true));
assertThat(listener.stateChangedCount, is(1));
assertThat(machine.getState().getIds(), containsInAnyOrder(States.S0));
listener.reset(1);
machine.sendEvent(Events.A);
assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true));
assertThat(listener.stateChangedCount, is(1));
assertThat(machine.getState().getIds(), containsInAnyOrder(States.S1));
}
private static void assertTransitionDenied(StateMachine<States, Events> machine, TestListener listener) throws Exception {
assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true));
assertThat(listener.stateChangedCount, is(1));
assertThat(machine.getState().getIds(), containsInAnyOrder(States.S0));
listener.reset(1);
machine.sendEvent(Events.A);
assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(false));
assertThat(listener.stateChangedCount, is(0));
assertThat(machine.getState().getIds(), containsInAnyOrder(States.S0));
}
@Configuration
public static class Config {
}
public static enum States {
S0, S1;
}
public static enum Events {
A;
}
private static class TestListener extends StateMachineListenerAdapter<States, Events> {
volatile CountDownLatch stateChangedLatch = new CountDownLatch(1);
volatile int stateChangedCount = 0;
@Override
public void stateChanged(State<States, Events> from, State<States, Events> to) {
stateChangedCount++;
stateChangedLatch.countDown();
}
public void reset(int c1) {
stateChangedLatch = new CountDownLatch(c1);
stateChangedCount = 0;
}
}
private static StateMachine<States, Events> buildMachine(TestListener listener, String attributes, ComparisonType match, String expression) throws Exception {
Builder<States, Events> builder = StateMachineBuilder.<States, Events>builder();
builder.configureConfiguration()
.withConfiguration()
.listener(listener)
.autoStartup(true)
.taskExecutor(new SyncTaskExecutor())
.and()
.withSecurity()
.enabled(true);
builder.configureStates()
.withStates()
.initial(States.S0)
.state(States.S0)
.state(States.S1);
ExternalTransitionConfigurer<States, Events> withExternal = builder.configureTransitions()
.withExternal();
if (attributes != null) {
withExternal.secured(attributes, match);
}
if (expression != null) {
withExternal.secured(expression);
}
withExternal.source(States.S0);
withExternal.target(States.S1);
withExternal.event(Events.A);
return builder.build();
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.statemachine.security;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.security.SecurityRule.ComparisonType;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.annotation.DirtiesContext.ClassMode;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* Tests for securing transitions.
*
* @author Janne Valkealahti
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@DirtiesContext(classMode = ClassMode.BEFORE_EACH_TEST_METHOD)
public class TransitionSecurityTests extends AbstractSecurityTests {
@Test
public void testNoSecurityContext() throws Exception {
TestListener listener = new TestListener();
StateMachine<States,Events> machine = buildMachine(listener, "ROLE_ANONYMOUS", ComparisonType.ANY, null);
assertTransitionDenied(machine, listener);
}
@Test
@WithMockUser(roles = { "ANONYMOUS" })
public void testTransitionAllowed() throws Exception {
TestListener listener = new TestListener();
StateMachine<States,Events> machine = buildMachine(listener, "ROLE_ANONYMOUS", ComparisonType.ANY, null);
assertTransitionAllowed(machine, listener);
}
@Test
@WithMockUser(roles = { "FOO" })
public void testTransitionDenied() throws Exception {
TestListener listener = new TestListener();
StateMachine<States,Events> machine = buildMachine(listener, "ROLE_ANONYMOUS", ComparisonType.ANY, null);
assertTransitionDenied(machine, listener);
}
@Test
@WithMockUser
public void testExpression() throws Exception {
TestListener listener = new TestListener();
StateMachine<States,Events> machine = buildMachine(listener, null, null, "hasTarget('S1')");
assertTransitionDenied(machine, listener);
}
@Configuration
public static class Config {
}
}

View File

@@ -211,11 +211,11 @@ public class StateChangeInterceptorTests extends AbstractStateMachineTests {
}
public static enum States {
S0, S1, S11, S12, S2, S21, S211, S212
S0, S1, S11, S12, S2, S21, S211, S212
}
public static enum Events {
A, B, C, D, E, F, G, H, I
A, B, C, D, E, F, G, H, I
}
private static class FooAction implements Action<States, Events> {
@@ -272,6 +272,11 @@ public class StateChangeInterceptorTests extends AbstractStateMachineTests {
volatile CountDownLatch preStateChangeLatch = new CountDownLatch(1);
volatile int preStateChangeCount = 0;
@Override
public Message<Events> preEvent(Message<Events> message, StateMachine<States, Events> stateMachine) {
return message;
}
@Override
public void preStateChange(State<States, Events> state, Message<Events> message,
Transition<States, Events> transition, StateMachine<States, Events> stateMachine) {

View File

@@ -36,6 +36,7 @@ import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.access.StateMachineAccessor;
import org.springframework.statemachine.action.Action;
import org.springframework.statemachine.listener.StateMachineListener;
import org.springframework.statemachine.security.SecurityRule;
import org.springframework.statemachine.state.EnumState;
import org.springframework.statemachine.state.State;
import org.springframework.statemachine.transition.Transition;
@@ -126,6 +127,11 @@ public class StateContextExpressionMethodsTests {
return null;
}
@Override
public SecurityRule getSecurityRule() {
return null;
}
}
private static class MockStatemachine implements StateMachine<SpelStates, SpelEvents> {