Add base security support
- Add base of support using spring security to protect events, transitions and actions. - Fixes #114
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -8,6 +8,7 @@ build
|
||||
*.log
|
||||
*.ipr
|
||||
*.iws
|
||||
*.swp
|
||||
metastore_db
|
||||
/src/test/resources/s3.properties
|
||||
/.idea/
|
||||
|
||||
18
build.gradle
18
build.gradle
@@ -27,7 +27,7 @@ configure(allprojects) {
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'eclipse'
|
||||
apply plugin: 'idea'
|
||||
|
||||
apply plugin: 'propdeps'
|
||||
|
||||
compileJava {
|
||||
sourceCompatibility = 1.7
|
||||
@@ -59,9 +59,9 @@ configure(allprojects) {
|
||||
// servlet-api (2.5) and tomcat-servlet-api (3.0) classpath entries should not be
|
||||
// exported to dependent projects in Eclipse to avoid false compilation errors due
|
||||
// to changing APIs across these versions
|
||||
eclipse.classpath.file.whenMerged { classpath ->
|
||||
classpath.entries.findAll { entry -> entry.path.contains('servlet-api') }*.exported = false
|
||||
}
|
||||
// eclipse.classpath.file.whenMerged { classpath ->
|
||||
// classpath.entries.findAll { entry -> entry.path.contains('servlet-api') }*.exported = false
|
||||
// }
|
||||
}
|
||||
|
||||
configure(subprojects) { subproject ->
|
||||
@@ -116,6 +116,7 @@ project('spring-statemachine-core') {
|
||||
dependencies {
|
||||
compile "org.springframework:spring-tx:$springVersion"
|
||||
compile "org.springframework:spring-messaging:$springVersion"
|
||||
optional "org.springframework.security:spring-security-core:$springSecurityVersion"
|
||||
|
||||
testCompile "org.springframework:spring-test:$springVersion"
|
||||
testCompile "org.springframework:spring-web:$springVersion"
|
||||
@@ -124,8 +125,14 @@ project('spring-statemachine-core') {
|
||||
testCompile "org.apache.tomcat.embed:tomcat-embed-logging-juli:$tomcatEmbedVersion"
|
||||
testCompile "org.hamcrest:hamcrest-core:$hamcrestVersion"
|
||||
testCompile "org.hamcrest:hamcrest-library:$hamcrestVersion"
|
||||
testCompile("org.mockito:mockito-core:$mockitoVersion") { dep ->
|
||||
exclude group: "org.hamcrest"
|
||||
}
|
||||
testCompile "junit:junit:$junitVersion"
|
||||
testRuntime("log4j:log4j:$log4jVersion")
|
||||
testCompile "org.springframework.security:spring-security-config:$springSecurityVersion"
|
||||
testCompile "org.springframework.security:spring-security-test:$springSecurityVersion"
|
||||
testCompile "javax.servlet:javax.servlet-api:$servletApiVersion"
|
||||
testRuntime "log4j:log4j:$log4jVersion"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,6 +271,7 @@ configure(rootProject) {
|
||||
from 'spring-statemachine-samples/cdplayer/src/main/java/'
|
||||
from 'spring-statemachine-samples/persist/src/main/java/'
|
||||
from 'spring-statemachine-samples/zookeeper/src/main/java/'
|
||||
from 'spring-statemachine-samples/security/src/main/java/'
|
||||
include '**/*.java'
|
||||
into 'docs/src/reference/asciidoc/samples'
|
||||
}
|
||||
|
||||
BIN
docs/src/reference/asciidoc/images/statechart13.png
Normal file
BIN
docs/src/reference/asciidoc/images/statechart13.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
@@ -28,6 +28,8 @@ normal build cycle. Samples in this chapter are:
|
||||
|
||||
<<statemachine-examples-scope>> Scope.
|
||||
|
||||
<<statemachine-examples-security>> Security.
|
||||
|
||||
|
||||
[source,text]
|
||||
----
|
||||
@@ -1070,3 +1072,62 @@ _Chrome_ and one in _Firefox_, you should get a new state machine
|
||||
instance per user session.
|
||||
|
||||
image::images/sm-scope-1.png[width=500]
|
||||
|
||||
[[statemachine-examples-security]]
|
||||
== Security
|
||||
Security is a state machine example using most of a compinations of
|
||||
securing a state machine. It is securing sending events, transitions
|
||||
and actions.
|
||||
|
||||
image::images/statechart13.png[width=500]
|
||||
|
||||
[source,text,subs="attributes"]
|
||||
----
|
||||
@n1:~# java -jar spring-statemachine-samples-secure-{revnumber}.jar
|
||||
----
|
||||
|
||||
We secure event sending with a users having a role `USER`. None of
|
||||
a other users imposed by a _Spring Security_ can't send events into a
|
||||
state machine.
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
include::samples/demo/security/StateMachineConfig.java[tags=snippetA]
|
||||
----
|
||||
|
||||
In this sample we define two users, _user_ having a role `USER` and
|
||||
_admin_ having both roles `USER` and `ADMIN`. Authentication for both
|
||||
user for password is `password`.
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
include::samples/demo/security/StateMachineConfig.java[tags=snippetE]
|
||||
----
|
||||
|
||||
We define various transitions between states according to a statechart
|
||||
seen above. Only a user with active `ADMIN` role can execute
|
||||
external transitions between `S2` and `S3`. Similarly `ADMIN` can only
|
||||
execute internal transition in a state `S1`.
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
include::samples/demo/security/StateMachineConfig.java[tags=snippetB]
|
||||
----
|
||||
|
||||
`Action` `adminAction` is secured with a role `ADMIN`.
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
include::samples/demo/security/StateMachineConfig.java[tags=snippetC]
|
||||
----
|
||||
|
||||
Below `Action` would only be executed with internal transition in a
|
||||
state `S1` when event `F` is send. Transition itself is secured with a
|
||||
role `ADMIN` so this transition will not be executed if current user
|
||||
does not hate that role.
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
include::samples/demo/security/StateMachineConfig.java[tags=snippetD]
|
||||
----
|
||||
|
||||
|
||||
@@ -894,6 +894,170 @@ More about error handling shown in above example, see section
|
||||
<<sm-error-handling>>.
|
||||
====
|
||||
|
||||
[[sm-security]]
|
||||
== State Machine Security
|
||||
Security features are build atop of functionality from a _Spring
|
||||
Security_. Security features are handy when it is required to protect
|
||||
part of a state machine execution and interaction with it. More
|
||||
detailed info can be found from section <<sm-security-details>>.
|
||||
|
||||
[IMPORTANT]
|
||||
====
|
||||
We expect user to be fairly familiar with a _Spring Security_ meaning
|
||||
we don't go into details of how overall security framework works. For
|
||||
this read _Spring Security_ reference documentation.
|
||||
====
|
||||
|
||||
[TIP]
|
||||
====
|
||||
For complete example, see sample <<statemachine-examples-security>>.
|
||||
====
|
||||
|
||||
=== Configuring Security
|
||||
All generic configurations for security are done from
|
||||
`SecurityConfigurer` which is obtained from
|
||||
`StateMachineConfigurationConfigurer`. Security is disabled on
|
||||
default even if _Spring Security_ classes are
|
||||
present.
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
include::samples/DocsConfigurationSampleTests3.java[tags=snippetD]
|
||||
----
|
||||
|
||||
If absolutely needed `AccessDecisionManager` for both events and
|
||||
transitions can be customised. If decision managers are not defined or
|
||||
are set to `null`, default managers are created internally.
|
||||
|
||||
=== Securing Events
|
||||
Event security is defined on a global level within a
|
||||
`SecurityConfigurer`.
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
include::samples/DocsConfigurationSampleTests3.java[tags=snippetA]
|
||||
----
|
||||
|
||||
In above configuration we use expression `true` which always evaluates
|
||||
to _TRUE_. Using an expression which always evaluates to _TRUE_
|
||||
would not make sense in a real application but gives a point that
|
||||
expression needs to return either _TRUE_ or _FALSE_. We also defined
|
||||
attribute `ROLE_ANONYMOUS` and `ComparisonType` `ANY`. Using attributes
|
||||
and expressions, see section <<sm-security-attributes-expressions>>.
|
||||
|
||||
=== Securing Transitions
|
||||
Transition security can be defined globally.
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
include::samples/DocsConfigurationSampleTests3.java[tags=snippetF]
|
||||
----
|
||||
|
||||
If security is defined in a transition itself it will override any
|
||||
globally set security.
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
include::samples/DocsConfigurationSampleTests3.java[tags=snippetB]
|
||||
----
|
||||
|
||||
Using attributes and expressions, see section <<sm-security-attributes-expressions>>.
|
||||
|
||||
=== Securing Actions
|
||||
There are no dedicated security definitions for actions in a state
|
||||
machine, but it can be accomplished using a global method security
|
||||
from a _Spring Security_. This simply needs that an `Action` is
|
||||
defined as a proxied `@Bean` and its `execute` method annotated with a
|
||||
`@Secured`.
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
include::samples/DocsConfigurationSampleTests3.java[tags=snippetC]
|
||||
----
|
||||
|
||||
Global method security needs to be enabled with a _Spring Security_
|
||||
which is done with along a lines shown below. See _Spring Security_
|
||||
reference docs for more details.
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
include::samples/DocsConfigurationSampleTests3.java[tags=snippetE]
|
||||
----
|
||||
|
||||
[[sm-security-attributes-expressions]]
|
||||
=== Using Security Attributes and Expressions
|
||||
Generally there are two ways to define security properties, firstly
|
||||
using security attributes and secondly using security expressions.
|
||||
Attributes are easier to use but are relatively limited in terms of
|
||||
functionality. Expressions provide more features but are a little bit
|
||||
of harder to use.
|
||||
|
||||
This section is splitted into different subsections because we're
|
||||
re-using something from a _Spring Security_ and then do custom
|
||||
attributes and expression handling for events and transitions.
|
||||
|
||||
==== Generic Attribute Usage
|
||||
On default `AccessDecisionManager` instances for events and
|
||||
transitions use a `RoleVoter`, meaning you can use role attributes.
|
||||
|
||||
For attributes we have 3 different comparison types, `ANY`, `ALL` and
|
||||
`MAJORITY` which maps into voters `AffirmativeBased`, `UnanimousBased`
|
||||
and `ConsensusBased` respectively.
|
||||
|
||||
==== Generic Expression Usage
|
||||
Security expressions needs to return either _TRUE_ or _FALSE_.
|
||||
|
||||
==== Event Attributes
|
||||
Event id can be matched by using prefix _EVENT__. For example matching
|
||||
event `A` would match with attribte _EVENT_A_.
|
||||
|
||||
==== Event Expressions
|
||||
For generic options for expressions, see _Spring Security_ docs. We
|
||||
inherit from `SecurityExpressionRoot` and options from there are
|
||||
available.
|
||||
|
||||
==== Transition Attributes
|
||||
Matching transition sources and targets, use prefixes
|
||||
_TRANSITION_SOURCE__ and _TRANSITION_TARGET__ respectively.
|
||||
|
||||
==== Transition Expressions
|
||||
For generic options for expressions, see _Spring Security_ docs. We
|
||||
inherit from `SecurityExpressionRoot` and options from there are
|
||||
available.
|
||||
|
||||
[[sm-security-details]]
|
||||
=== Understanding Security
|
||||
This section provides more detailed info how security works within a
|
||||
state machine. Not really something you'd need to know but it is
|
||||
always better to be transparent instead of hiding all the magic what
|
||||
happens behind a scenes.
|
||||
|
||||
Security only makes sense if _State Machine_ is executed in a wallet
|
||||
garden where user don't have direct access to the application thus
|
||||
could modify Spring Security's `SecurityContext` hold in a thread
|
||||
local. If user controls the jvm, then effectively there is no security
|
||||
at all.
|
||||
|
||||
Integration point for security is done with a
|
||||
`StateMachineInterceptor` which is then added automatically into a
|
||||
state machine if security is enabled. Specific class is a
|
||||
`StateMachineSecurityInterceptor` which intercepts events and
|
||||
transitions. This interceptor then consults Spring Security's
|
||||
`AccessDecisionManager` if event can be send or if transition can be
|
||||
executed. Effectively if decision or vote with a `AccessDecisionManager`
|
||||
will result an exception, event or transition is denied.
|
||||
|
||||
Due to way how `AccessDecisionManager` from Spring Security works, we
|
||||
need one instance of it per secured object. This is a reason why there
|
||||
is a different manager for events and transitions. In this case events
|
||||
and transitions are different class objects we're securing.
|
||||
|
||||
On default for events, voters `EventExpressionVoter`, `EventVoter` and
|
||||
`RoleVoter` are added into a `AccessDecisionManager`.
|
||||
|
||||
On default for transitions, voters `TransitionExpressionVoter`,
|
||||
`TransitionVoter` and `RoleVoter` are added into a `AccessDecisionManager`.
|
||||
|
||||
[[sm-error-handling]]
|
||||
== State Machine Error Handling
|
||||
If state machine detects an internal error during a state transition
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
springShellVersion=1.1.0.RELEASE
|
||||
springSecurityVersion=4.0.2.RELEASE
|
||||
springSecurityVersion=4.0.3.RELEASE
|
||||
junitVersion=4.12
|
||||
springVersion=4.2.2.RELEASE
|
||||
kryoVersion=2.24.0
|
||||
log4jVersion=1.2.17
|
||||
springBootVersion=1.2.5.RELEASE
|
||||
version=1.1.0.BUILD-SNAPSHOT
|
||||
mockitoVersion = 1.9.5
|
||||
hamcrestVersion=1.3
|
||||
springSessionVersion=1.0.2.RELEASE
|
||||
curatorVersion=2.8.0
|
||||
tomcatEmbedVersion=7.0.56
|
||||
servletApiVersion=3.0.1
|
||||
|
||||
@@ -16,6 +16,7 @@ include 'spring-statemachine-samples:zookeeper'
|
||||
include 'spring-statemachine-samples:persist'
|
||||
include 'spring-statemachine-samples:web'
|
||||
include 'spring-statemachine-samples:scope'
|
||||
include 'spring-statemachine-samples:security'
|
||||
|
||||
rootProject.children.find {
|
||||
if (it.name == 'spring-statemachine-recipes') {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 + "]";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 + "]";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -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[]
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -54,3 +54,13 @@ project('spring-statemachine-samples-scope') {
|
||||
// compile("org.springframework:spring-webmvc:$springVersion")
|
||||
}
|
||||
}
|
||||
|
||||
project('spring-statemachine-samples-security') {
|
||||
description = 'Spring State Machine Web Security Sample'
|
||||
dependencies {
|
||||
compile("org.springframework.boot:spring-boot-starter-thymeleaf:$springBootVersion")
|
||||
compile("org.springframework.boot:spring-boot-starter-security:$springBootVersion")
|
||||
compile("org.springframework.security:spring-security-config:$springSecurityVersion")
|
||||
compile("org.springframework.security:spring-security-web:$springSecurityVersion")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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 demo.security;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class Application {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(Application.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* 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 demo.security;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Scanner;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
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.core.io.ClassPathResource;
|
||||
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.EnableWebSecurity;
|
||||
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.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.security.SecurityRule.ComparisonType;
|
||||
|
||||
@Configuration
|
||||
public class StateMachineConfig {
|
||||
|
||||
private static final Log log = LogFactory.getLog(StateMachineConfig.class);
|
||||
|
||||
//tag::snippetE[]
|
||||
@EnableWebSecurity
|
||||
@EnableGlobalMethodSecurity(securedEnabled = true)
|
||||
static class SecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
|
||||
@Autowired
|
||||
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
|
||||
auth
|
||||
.inMemoryAuthentication()
|
||||
.withUser("user")
|
||||
.password("password")
|
||||
.roles("USER")
|
||||
.and()
|
||||
.withUser("admin")
|
||||
.password("password")
|
||||
.roles("USER", "ADMIN");
|
||||
}
|
||||
}
|
||||
//end::snippetE[]
|
||||
|
||||
@Configuration
|
||||
@EnableStateMachine
|
||||
static class Config
|
||||
extends EnumStateMachineConfigurerAdapter<States, Events> {
|
||||
|
||||
//tag::snippetA[]
|
||||
@Override
|
||||
public void configure(StateMachineConfigurationConfigurer<States, Events> config)
|
||||
throws Exception {
|
||||
config
|
||||
.withConfiguration()
|
||||
.autoStartup(true)
|
||||
.and()
|
||||
.withSecurity()
|
||||
.enabled(true)
|
||||
.event("hasRole('USER')");
|
||||
}
|
||||
//end::snippetA[]
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer<States, Events> states)
|
||||
throws Exception {
|
||||
states
|
||||
.withStates()
|
||||
.initial(States.S0)
|
||||
.states(EnumSet.allOf(States.class));
|
||||
}
|
||||
|
||||
//tag::snippetB[]
|
||||
@Override
|
||||
public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
|
||||
throws Exception {
|
||||
transitions
|
||||
.withExternal()
|
||||
.source(States.S0).target(States.S1).event(Events.A)
|
||||
.and()
|
||||
.withExternal()
|
||||
.source(States.S1).target(States.S2).event(Events.B)
|
||||
.and()
|
||||
.withExternal()
|
||||
.source(States.S2).target(States.S0).event(Events.C)
|
||||
.and()
|
||||
.withExternal()
|
||||
.source(States.S2).target(States.S3).event(Events.E)
|
||||
.secured("ROLE_ADMIN", ComparisonType.ANY)
|
||||
.and()
|
||||
.withExternal()
|
||||
.source(States.S3).target(States.S0).event(Events.C)
|
||||
.and()
|
||||
.withInternal()
|
||||
.source(States.S0).event(Events.D)
|
||||
.action(adminAction())
|
||||
.and()
|
||||
.withInternal()
|
||||
.source(States.S1).event(Events.F)
|
||||
.action(transitionAction())
|
||||
.secured("ROLE_ADMIN", ComparisonType.ANY);
|
||||
}
|
||||
//end::snippetB[]
|
||||
|
||||
//tag::snippetC[]
|
||||
@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)
|
||||
@Bean
|
||||
public Action<States, Events> adminAction() {
|
||||
return new Action<States, Events>() {
|
||||
|
||||
@Secured("ROLE_ADMIN")
|
||||
@Override
|
||||
public void execute(StateContext<States, Events> context) {
|
||||
log.info("Executed only for admin role");
|
||||
}
|
||||
};
|
||||
}
|
||||
//end::snippetC[]
|
||||
|
||||
//tag::snippetD[]
|
||||
@Bean
|
||||
public Action<States, Events> transitionAction() {
|
||||
return new Action<States, Events>() {
|
||||
|
||||
@Override
|
||||
public void execute(StateContext<States, Events> context) {
|
||||
log.info("Executed only for admin role");
|
||||
}
|
||||
};
|
||||
}
|
||||
//end::snippetD[]
|
||||
}
|
||||
|
||||
@Bean
|
||||
public String stateChartModel() throws IOException {
|
||||
ClassPathResource model = new ClassPathResource("statechartmodel.txt");
|
||||
InputStream inputStream = model.getInputStream();
|
||||
Scanner scanner = new Scanner(inputStream);
|
||||
String content = scanner.useDelimiter("\\Z").next();
|
||||
scanner.close();
|
||||
return content;
|
||||
}
|
||||
|
||||
public enum States {
|
||||
S0, S1, S2, S3;
|
||||
}
|
||||
|
||||
public enum Events {
|
||||
A, B, C, D, E, F;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 demo.security;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.statemachine.StateMachine;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import demo.security.StateMachineConfig.Events;
|
||||
import demo.security.StateMachineConfig.States;
|
||||
|
||||
@Controller
|
||||
public class StateMachineController {
|
||||
|
||||
private final static Log log = LogFactory.getLog(StateMachineController.class);
|
||||
|
||||
@Autowired
|
||||
private StateMachine<States, Events> stateMachine;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("stateChartModel")
|
||||
private String stateChartModel;
|
||||
|
||||
@RequestMapping("/")
|
||||
public String greeting() {
|
||||
return "redirect:/states";
|
||||
}
|
||||
|
||||
@RequestMapping("/states")
|
||||
public String getStates(@RequestParam(value = "event", required = false) Events event, Model model) {
|
||||
if (event != null) {
|
||||
try {
|
||||
stateMachine.sendEvent(event);
|
||||
} catch (Exception e) {
|
||||
log.error("Error sendEvent", e);
|
||||
}
|
||||
}
|
||||
model.addAttribute("states", stateMachine.getState().getIds());
|
||||
model.addAttribute("stateChartModel", stateChartModel);
|
||||
return "states";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
security:
|
||||
basic:
|
||||
enabled: false
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
|
||||
<include resource="org/springframework/boot/logging/logback/base.xml"/>
|
||||
|
||||
<logger name="org.springframework.statemachine" level="TRACE"/>
|
||||
|
||||
</configuration>
|
||||
@@ -0,0 +1,19 @@
|
||||
+--------------------------------------------------------------------------+
|
||||
| SM |
|
||||
+--------------------------------------------------------------------------+
|
||||
| |
|
||||
| +-----------+ A +-----------+ B +--------+ E +--------+ |
|
||||
| *-->| S0 |------>| S1 |------>| S2 |------>| S3 | |
|
||||
| | | | | | | | | |
|
||||
| | D | | F | | | | | |
|
||||
| | +-------+ | | +-------+ | | | | | |
|
||||
| | | | | | | | | | | | | |
|
||||
| | | v | | | v | | | | | |
|
||||
| +-----------+ +-----------+ +--------+ +--------+ |
|
||||
| ^ ^ | | |
|
||||
| | | C | | |
|
||||
| | +------------------------------------+ | |
|
||||
| | C | |
|
||||
| +----------------------------------------------------------+ |
|
||||
| |
|
||||
+--------------------------------------------------------------------------+
|
||||
@@ -0,0 +1,27 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<title>Spring Statemachine Scope Demo</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
</head>
|
||||
<body>
|
||||
<div th:with="currentUser=${#httpServletRequest.userPrincipal?.principal}" >
|
||||
<div th:if="${currentUser != null}">
|
||||
<form th:action="@{/logout}" method="post">
|
||||
<input type="submit" value="Log out" />
|
||||
</form>
|
||||
<p th:text="'CurrentUser: ' + ${currentUser.username}"/>
|
||||
</div>
|
||||
</div>
|
||||
<p th:text="'States: ' + ${states}" />
|
||||
<form action="#" data-th-action="@{/states}" data-th-object="${model}" method="post">
|
||||
<button type="submit" name="event" value="A">Send A</button>
|
||||
<button type="submit" name="event" value="B">Send B</button>
|
||||
<button type="submit" name="event" value="C">Send C</button>
|
||||
<button type="submit" name="event" value="D">Send D</button>
|
||||
<button type="submit" name="event" value="E">Send E</button>
|
||||
<button type="submit" name="event" value="F">Send F</button>
|
||||
</form>
|
||||
<pre th:text="${stateChartModel}"/>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user