Add base security support

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

1
.gitignore vendored
View File

@@ -8,6 +8,7 @@ build
*.log
*.ipr
*.iws
*.swp
metastore_db
/src/test/resources/s3.properties
/.idea/

View File

@@ -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'
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@@ -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]
----

View File

@@ -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

View File

@@ -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

View File

@@ -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') {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,171 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.statemachine.docs;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.statemachine.StateContext;
import org.springframework.statemachine.action.Action;
import org.springframework.statemachine.config.EnableStateMachine;
import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
import org.springframework.statemachine.security.SecurityRule.ComparisonType;
public class DocsConfigurationSampleTests3 {
// tag::snippetA[]
@Configuration
@EnableStateMachine
static class Config1 extends StateMachineConfigurerAdapter<String, String> {
@Override
public void configure(StateMachineConfigurationConfigurer<String, String> config)
throws Exception {
config
.withSecurity()
.enabled(true)
.event("true")
.event("ROLE_ANONYMOUS", ComparisonType.ANY);
}
}
// end::snippetA[]
// tag::snippetB[]
@Configuration
@EnableStateMachine
static class Config2 extends StateMachineConfigurerAdapter<String, String> {
@Override
public void configure(StateMachineTransitionConfigurer<String, String> transitions)
throws Exception {
transitions
.withExternal()
.source("S0")
.target("S1")
.event("A")
.secured("ROLE_ANONYMOUS", ComparisonType.ANY)
.secured("hasTarget('S1')");
}
}
// end::snippetB[]
// tag::snippetC[]
@Configuration
@EnableStateMachine
static class Config3 extends StateMachineConfigurerAdapter<String, String> {
@Override
public void configure(StateMachineConfigurationConfigurer<String, String> config)
throws Exception {
config
.withSecurity()
.enabled(true);
}
@Override
public void configure(StateMachineStateConfigurer<String, String> states)
throws Exception {
states
.withStates()
.initial("S0")
.state("S1");
}
@Override
public void configure(StateMachineTransitionConfigurer<String, String> transitions)
throws Exception {
transitions
.withExternal()
.source("S0")
.target("S1")
.action(securedAction())
.event("A");
}
@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)
@Bean
public Action<String, String> securedAction() {
return new Action<String, String>() {
@Secured("ROLE_ANONYMOUS")
@Override
public void execute(StateContext<String, String> context) {
}
};
}
}
// end::snippetC[]
// tag::snippetD[]
@Configuration
@EnableStateMachine
static class Config4 extends StateMachineConfigurerAdapter<String, String> {
@Override
public void configure(StateMachineConfigurationConfigurer<String, String> config)
throws Exception {
config
.withSecurity()
.enabled(true)
.transitionAccessDecisionManager(null)
.eventAccessDecisionManager(null);
}
}
// end::snippetD[]
// tag::snippetE[]
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true)
public static class Config5 extends WebSecurityConfigurerAdapter {
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("user").password("password").roles("USER");
}
}
// end::snippetE[]
// tag::snippetF[]
@Configuration
@EnableStateMachine
static class Config6 extends StateMachineConfigurerAdapter<String, String> {
@Override
public void configure(StateMachineConfigurationConfigurer<String, String> config)
throws Exception {
config
.withSecurity()
.enabled(true)
.transition("true")
.transition("ROLE_ANONYMOUS", ComparisonType.ANY);
}
}
// end::snippetF[]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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")
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package 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";
}
}

View File

@@ -0,0 +1,3 @@
security:
basic:
enabled: false

View File

@@ -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>

View File

@@ -0,0 +1,19 @@
+--------------------------------------------------------------------------+
| SM |
+--------------------------------------------------------------------------+
| |
| +-----------+ A +-----------+ B +--------+ E +--------+ |
| *-->| S0 |------>| S1 |------>| S2 |------>| S3 | |
| | | | | | | | | |
| | D | | F | | | | | |
| | +-------+ | | +-------+ | | | | | |
| | | | | | | | | | | | | |
| | | v | | | v | | | | | |
| +-----------+ +-----------+ +--------+ +--------+ |
| ^ ^ | | |
| | | C | | |
| | +------------------------------------+ | |
| | C | |
| +----------------------------------------------------------+ |
| |
+--------------------------------------------------------------------------+

View File

@@ -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>