From d0aaa4bfee98ab708b9b5b5b210e2e6f6dbc5b8a Mon Sep 17 00:00:00 2001 From: Janne Valkealahti Date: Sun, 20 Dec 2015 14:33:54 +0000 Subject: [PATCH] Add base security support - Add base of support using spring security to protect events, transitions and actions. - Fixes #114 --- .gitignore | 1 + build.gradle | 18 +- .../asciidoc/images/statechart13.png | Bin 0 -> 5469 bytes docs/src/reference/asciidoc/sm-examples.adoc | 61 +++ docs/src/reference/asciidoc/sm.adoc | 164 ++++++ gradle.properties | 4 +- settings.gradle | 1 + .../config/AbstractStateMachineFactory.java | 40 +- .../builders/StateMachineConfigBuilder.java | 15 +- .../StateMachineConfigurationBuilder.java | 63 ++- .../StateMachineConfigurationConfig.java | 66 ++- .../StateMachineConfigurationConfigurer.java | 9 + .../StateMachineSecurityConfigurer.java | 38 ++ .../StateMachineTransitionBuilder.java | 11 +- .../builders/StateMachineTransitions.java | 15 +- .../AbstractTransitionConfigurer.java | 124 +++++ .../DefaultExternalTransitionConfigurer.java | 55 +- .../DefaultInternalTransitionConfigurer.java | 53 +- .../DefaultLocalTransitionConfigurer.java | 56 +- .../DefaultSecurityConfigurer.java | 109 ++++ .../ExternalTransitionConfigurer.java | 2 +- .../configurers/SecurityConfigurer.java | 93 ++++ .../configurers/TransitionConfigurer.java | 23 +- .../ensemble/DistributedStateMachine.java | 5 + ...DefaultEventSecurityExpressionHandler.java | 56 ++ ...ltTransitionSecurityExpressionHandler.java | 78 +++ .../EventExpressionConfigAttribute.java | 56 ++ .../security/EventExpressionVoter.java | 86 ++++ .../security/EventSecurityExpressionRoot.java | 36 ++ .../statemachine/security/EventVoter.java | 86 ++++ .../statemachine/security/SecurityRule.java | 141 ++++++ .../StateMachineSecurityInterceptor.java | 239 +++++++++ .../TransitionExpressionConfigAttribute.java | 62 +++ .../security/TransitionExpressionVoter.java | 67 +++ .../TransitionSecurityExpressionRoot.java | 52 ++ .../security/TransitionVoter.java | 124 +++++ .../statemachine/state/ObjectState.java | 16 +- .../support/AbstractStateMachine.java | 9 + .../support/DefaultStateMachineExecutor.java | 10 +- .../support/StateMachineInterceptor.java | 10 + .../StateMachineInterceptorAdapter.java | 5 + .../support/StateMachineInterceptorList.java | 16 + .../AbstractExternalTransition.java | 10 +- .../AbstractInternalTransition.java | 11 +- .../transition/AbstractLocalTransition.java | 11 +- .../transition/AbstractTransition.java | 16 +- .../transition/DefaultExternalTransition.java | 9 +- .../transition/DefaultInternalTransition.java | 9 +- .../transition/InitialTransition.java | 7 + .../statemachine/transition/Transition.java | 12 +- .../docs/DocsConfigurationSampleTests.java | 21 +- .../docs/DocsConfigurationSampleTests3.java | 171 +++++++ .../security/AbstractSecurityTests.java | 136 +++++ .../security/ActionSecurityTests.java | 208 ++++++++ .../security/EventSecurityTests.java | 69 +++ .../security/SecurityConfigTests.java | 479 ++++++++++++++++++ .../security/SecurityRuleTests.java | 50 ++ .../TransitionSecurityAttributeTests.java | 78 +++ ...TransitionSecurityExpressionRootTests.java | 133 +++++ .../TransitionSecurityExpressionTests.java | 156 ++++++ .../security/TransitionSecurityTests.java | 74 +++ .../support/StateChangeInterceptorTests.java | 9 +- .../StateContextExpressionMethodsTests.java | 6 + spring-statemachine-samples/build.gradle | 10 + .../main/java/demo/security/Application.java | 27 + .../demo/security/StateMachineConfig.java | 177 +++++++ .../demo/security/StateMachineController.java | 62 +++ .../src/main/resources/application.yml | 3 + .../security/src/main/resources/logback.xml | 8 + .../src/main/resources/statechartmodel.txt | 19 + .../src/main/resources/templates/states.html | 27 + 71 files changed, 4037 insertions(+), 146 deletions(-) create mode 100644 docs/src/reference/asciidoc/images/statechart13.png create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineSecurityConfigurer.java create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/AbstractTransitionConfigurer.java create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultSecurityConfigurer.java create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/SecurityConfigurer.java create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/security/DefaultEventSecurityExpressionHandler.java create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/security/DefaultTransitionSecurityExpressionHandler.java create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/security/EventExpressionConfigAttribute.java create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/security/EventExpressionVoter.java create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/security/EventSecurityExpressionRoot.java create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/security/EventVoter.java create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/security/SecurityRule.java create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/security/StateMachineSecurityInterceptor.java create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/security/TransitionExpressionConfigAttribute.java create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/security/TransitionExpressionVoter.java create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/security/TransitionSecurityExpressionRoot.java create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/security/TransitionVoter.java create mode 100644 spring-statemachine-core/src/test/java/org/springframework/statemachine/docs/DocsConfigurationSampleTests3.java create mode 100644 spring-statemachine-core/src/test/java/org/springframework/statemachine/security/AbstractSecurityTests.java create mode 100644 spring-statemachine-core/src/test/java/org/springframework/statemachine/security/ActionSecurityTests.java create mode 100644 spring-statemachine-core/src/test/java/org/springframework/statemachine/security/EventSecurityTests.java create mode 100644 spring-statemachine-core/src/test/java/org/springframework/statemachine/security/SecurityConfigTests.java create mode 100644 spring-statemachine-core/src/test/java/org/springframework/statemachine/security/SecurityRuleTests.java create mode 100644 spring-statemachine-core/src/test/java/org/springframework/statemachine/security/TransitionSecurityAttributeTests.java create mode 100644 spring-statemachine-core/src/test/java/org/springframework/statemachine/security/TransitionSecurityExpressionRootTests.java create mode 100644 spring-statemachine-core/src/test/java/org/springframework/statemachine/security/TransitionSecurityExpressionTests.java create mode 100644 spring-statemachine-core/src/test/java/org/springframework/statemachine/security/TransitionSecurityTests.java create mode 100644 spring-statemachine-samples/security/src/main/java/demo/security/Application.java create mode 100644 spring-statemachine-samples/security/src/main/java/demo/security/StateMachineConfig.java create mode 100644 spring-statemachine-samples/security/src/main/java/demo/security/StateMachineController.java create mode 100644 spring-statemachine-samples/security/src/main/resources/application.yml create mode 100644 spring-statemachine-samples/security/src/main/resources/logback.xml create mode 100644 spring-statemachine-samples/security/src/main/resources/statechartmodel.txt create mode 100644 spring-statemachine-samples/security/src/main/resources/templates/states.html diff --git a/.gitignore b/.gitignore index 1100e6c2..3411683f 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ build *.log *.ipr *.iws +*.swp metastore_db /src/test/resources/s3.properties /.idea/ diff --git a/build.gradle b/build.gradle index 9678d7ca..ca094114 100644 --- a/build.gradle +++ b/build.gradle @@ -27,7 +27,7 @@ configure(allprojects) { apply plugin: 'java' apply plugin: 'eclipse' apply plugin: 'idea' - + apply plugin: 'propdeps' compileJava { sourceCompatibility = 1.7 @@ -59,9 +59,9 @@ configure(allprojects) { // servlet-api (2.5) and tomcat-servlet-api (3.0) classpath entries should not be // exported to dependent projects in Eclipse to avoid false compilation errors due // to changing APIs across these versions - eclipse.classpath.file.whenMerged { classpath -> - classpath.entries.findAll { entry -> entry.path.contains('servlet-api') }*.exported = false - } +// eclipse.classpath.file.whenMerged { classpath -> +// classpath.entries.findAll { entry -> entry.path.contains('servlet-api') }*.exported = false +// } } configure(subprojects) { subproject -> @@ -116,6 +116,7 @@ project('spring-statemachine-core') { dependencies { compile "org.springframework:spring-tx:$springVersion" compile "org.springframework:spring-messaging:$springVersion" + optional "org.springframework.security:spring-security-core:$springSecurityVersion" testCompile "org.springframework:spring-test:$springVersion" testCompile "org.springframework:spring-web:$springVersion" @@ -124,8 +125,14 @@ project('spring-statemachine-core') { testCompile "org.apache.tomcat.embed:tomcat-embed-logging-juli:$tomcatEmbedVersion" testCompile "org.hamcrest:hamcrest-core:$hamcrestVersion" testCompile "org.hamcrest:hamcrest-library:$hamcrestVersion" + testCompile("org.mockito:mockito-core:$mockitoVersion") { dep -> + exclude group: "org.hamcrest" + } testCompile "junit:junit:$junitVersion" - testRuntime("log4j:log4j:$log4jVersion") + testCompile "org.springframework.security:spring-security-config:$springSecurityVersion" + testCompile "org.springframework.security:spring-security-test:$springSecurityVersion" + testCompile "javax.servlet:javax.servlet-api:$servletApiVersion" + testRuntime "log4j:log4j:$log4jVersion" } } @@ -264,6 +271,7 @@ configure(rootProject) { from 'spring-statemachine-samples/cdplayer/src/main/java/' from 'spring-statemachine-samples/persist/src/main/java/' from 'spring-statemachine-samples/zookeeper/src/main/java/' + from 'spring-statemachine-samples/security/src/main/java/' include '**/*.java' into 'docs/src/reference/asciidoc/samples' } diff --git a/docs/src/reference/asciidoc/images/statechart13.png b/docs/src/reference/asciidoc/images/statechart13.png new file mode 100644 index 0000000000000000000000000000000000000000..cb22f18c0c107f938e5bd605df8301c7cbe9c50d GIT binary patch literal 5469 zcmcIoc{tQ-`~S|M&~{X)gi5JnETv-!(S|G`F*Cx<*dp7=kljovQ8LlNt1L$)VXSkM zNsKzytS8ymlx^%X_T_zMEU&uW_gue!esf)}%lBFC=UzVd=f0mO*g#Kn8}B||2!gh0 zUA$ljK^zAl2zHJe4n_jAq%|Rk|FG7DvqqjR!|gXNex6?5JXR=->bJOc@lHsPhWcaq zps^i1eDD7l-f<#9?ZTW@$ioxI9DfnJR=@7TU@9|$^ZA1nYt+-L|= z-otlO|{Z7XZ&e+# zO>S+X0L_Yj4X>P;@sxiW8xvF6Gm{I{ z$gCwXiv_X1->VeV7ANa+92$XaPDv1?Ezp@l)&Ve`LO8FHBSwb4;HnghdF|Nz^3v05 zqs|tC4QVH*i-s3spKJn#?4Vlw71(rQZhju$S&(mA6L$)RFw6IrQym^0d`i=-0|L7& z43mw9_Hb};OpiJz6cj8m8?j{lPH=Xv6b=R#D10ubpimmhcOo@8d7$fkz#DvLoN%VM zw|BSB;^N}?3Fj?n;N3Ah1$;*vok(N?dTg~1%EoXznxK>*JGXYxBt>? z-@+9W0=&UC-cLZG`#Fh=8-4Rk7xuc7Q@-24r+hlLB#!u!an?k;5Y8z{(sa0fJvlvH zFVS$o?QKHB!DZzDxWEA%4;mn0J-)1{aice&gZ*&;$FB!GehhPWPv(c2nVH&J11uJc z$B#yzv@L+3VKcI-(|L#! zLigkOcvpaX#$q!C?zb&62L|0&!0~spC(c~nOgZN(DhWb*j6v$b^TAh;`IABaH*0y& zV+PHR@)6p_)+MF6X$K{JzsFuiA^|M2rA%M@j=kSpS6BDsiap2ir`0S6CH6elR}Idh zP$(f}QQ|^Y;G?oxrSAg|g%ny^TffGg_L;KNS+lLFC&)$v2co{uT&7jZJ#_vOCUADeV+ofF(ss z(Q^z}`R$&l|DHErt=d9;{=Cas)A&>Yf78va=rq{+L=1Njg!cD4f*8*P3T0@HIe25f zMr}EyRY=~kRzSeNp!zZfvs58Nx6>=%PNM3AG!Xe}fAEIBmkn`YymUqr08^DvlbTVm zZ@Ng%D!tO!P#>qOtNV`GU`i0LGLvekC%&%1nT#Y+=itcjwB1dDHu2v}I~F_q=bZ>P z={r@hqiwWZ(~_pIE6pN4j4V2GM5+^c+keRnp{S}=PENWR8ykChdF}V7?)ad{g`%Lx zcXwo$nem`|wF|T7fRV+Pg9Hx`kA(+$H;11Ph)c|}B)wQ!RIfzXclTa8x7D9RD0pPO zAn=%2UfF!xr%yYVyPK)DeQl!CMT_IcKnDTWf@=Sh(@&l}sjjY0`tAX*G`_`2v7N9# zo+^VgIXu~HHbC`uKc5c{yQ`Xtimy)R2 z6>tcZo{*7|L8Ey!V}Ul^R|ErSS7*!?$Nt)}ZQC~WIp@#443kTGcs$<6$4C5ea7MZb zwYTsV2eor&F+*mgo_t9RNl!_UDA?C`83fnXwhusylX80tx-?X{^_>?%U0vPWyq&&v zV%gRDFCCfu&z+sv-BJAjdEM{#tNHtSdn-j%_$@LU`I4>`C>=kZX<2+fw{Hr22v?kR zBPn2P0Dt$v_4IRAR#uKt@JO0Br99Z6qrF{tx>PHM`27Bie=1ffl{oROJUBQwSGezQ z<(Zr5^yMX=gV<-$y5b_0fTfj`mBnsUdlmRG8TI4!Gg^n1 zueSy1#}k*lHh-bE@Cuf)AZ35+@vd|%U8FNwFDcERC*41$3j~R1KbnKUEt-P785tOe zrW#d5$q;vn2}wvu=*1{r&W!3uVf~EF%}>v7+uGXFV%hhRD5o0x-5|a^^cup2vCD1Q^%fZtJWpw(9rhI>q?=-A0&Pg#**!U_%>`uXxM4*Njso6AdbH3 zSfAmX#tHqJ!06>EOdAX#p5VFs%;$1VefM_Yi(hs1eC0;;6Ae54=hNzCa_V=`7pF{B zE#AC-&5P#RoylM@q_G7m^Kn?>qC4PN{chXaGjEp0>&>}*UVV#qiw!nddXRaX*dRGY zo7fsjyIS_`8?-gWza#YaT# zuS;sl&P>5YMk&mV@j2P|m;E9l&Q4AbGYXcM=HW=5{J;B!@bAXU=;sU+qwFf}K;kSY zqLIi*?L>L!HqWuH_x~D-#^-u7?Fj_JoD4wLcyd6knUrt($g(IepbwE#rAy!r zB-Ral{P+>HY||Y+qhz(ET3e>L?A7VIwS}ohaOBcdQBl$O*HQ(?rbp%0Z}ak)Ciz+q zBiHpz6v~{)ZXPOmC{#BvS)Ymm(}0COZ3D&wRUej7$8KNUEGkjSR#iZXEm zZ?7vYmztez7+;UTMI(Tk&fI7X`K&9)M|2Yn=O`Uqp^<{LHNU|dh+dm?^7W&as_`j50_z>HD)-RRJ95!mIJ;+`{)7F>@Q) z=(;3*AZ3F#)yuoL&U(1wot;%(x=0?bsEvEy?)m&+-CSS=uy6$cvf1n2|3`mUlmTR{ z>|)o@2WRv9slY{`lJTHf|E>A^$A!FhVUfR!m*j%J}CrTD(dH%hoPa;=5HrOcYG)) zP|XKD%en_PfAMu}eLcK%B@=Yw8I=(<*P@jKuQ;oye^f6fclm&dTmPdU1-is#H#@4U z7smv+xVahqw2L@AJWSPp|MI2y#)BwFb1s5vCUf_cs>ev%P{3GImeuP`fQ8n>n7suw zU=eCu2nh*rt4OS2$f0_*SNY{;!qiCnQ#Ednp(c>?8z$sof-;y&(L1M4p9Zi6;@sHX z6Ufml|HZkKl$7M;UjdZ>w5_4Lyr02n$@h7~Vik6R0v_4hTvQZep6hZpzTP3dl7)s< z;#qIOD$3RphEnho@%)JI5Xwp-WY2M<1$ayo)En3G;0MMm&k0X@{#n`|I6Qj}N$*Eu z!FA>raPi}0T^Lwh_m7uBZ+S~&X|vp}%P`j6=*Y{|REO!2b`gHo-3V!k@f|N-v6alr zl7L`)g28YFWrkR^MS1v!Fg zzO$~NTLouGL}(|~r6SYpot)|gh9rQ(6-~4~|4I_m3@u`47Vg`>KSZBEGXuRmfk3Us zq22?1N~((seT($I2rp!%;+R~LR(+NSv_OTh2Sx>3ZK>76EHmbbD9Cmy|9n8ytRzvFOS9i5{A^-WE$ zPOi@$Cq_w|Gy3~qrKd|H5ITv5a&mH@?6=%rm%pa{bNG-uJ2K$)U`PU>A~K8055LI| z{BJX)B2kFB$G(}swiKq{W+7hL^x)doVyJ159XoL8ZZc*EY;WE&(FiKf{-Qhp;8f8B zBR$qDl)VXcz1Rh=gpkW89@YUI(9u{R9V*m13IzUV6jXiLn}zh7vlBg2Q&XTjG?IwW zOG2ext#4^bkz(EOOSZ2>gMS~)!NujcYE_SGdwY9TRh1G4z>v)lX*S63q;w77khGOE zNY;PmmMbW|NLOP!MK7d0dzT)l3y6=lVMOHYZV`Fa3g zE8pe7`>PWEo9kD#qw7RTy8~_mC|~fd z?-860wHdr=ar>dt&4Np(w?SvdgC=+G6%W__7JX6f?#@i{qen|iN&@fQ`x>S`H_rq8 z;d#M3cl1)%@{ literal 0 HcmV?d00001 diff --git a/docs/src/reference/asciidoc/sm-examples.adoc b/docs/src/reference/asciidoc/sm-examples.adoc index 017a64ca..e584dbf6 100644 --- a/docs/src/reference/asciidoc/sm-examples.adoc +++ b/docs/src/reference/asciidoc/sm-examples.adoc @@ -28,6 +28,8 @@ normal build cycle. Samples in this chapter are: <> Scope. +<> 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] +---- + diff --git a/docs/src/reference/asciidoc/sm.adoc b/docs/src/reference/asciidoc/sm.adoc index d5d7316b..c8e559ac 100644 --- a/docs/src/reference/asciidoc/sm.adoc +++ b/docs/src/reference/asciidoc/sm.adoc @@ -894,6 +894,170 @@ More about error handling shown in above example, see section <>. ==== +[[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 <>. + +[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 <>. +==== + +=== 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 <>. + +=== 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 <>. + +=== 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 diff --git a/gradle.properties b/gradle.properties index 818369de..c2a946dd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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 diff --git a/settings.gradle b/settings.gradle index b13782aa..31ce80b2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -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') { diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/AbstractStateMachineFactory.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/AbstractStateMachineFactory.java index 413000e3..92e932ea 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/AbstractStateMachineFactory.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/AbstractStateMachineFactory.java @@ -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 extends LifecycleObjectSupport implements StateMachineFactory, BeanNameAware { + private final Log log = LogFactory.getLog(AbstractStateMachineFactory.class); + private final StateMachineTransitions stateMachineTransitions; private final StateMachineStates stateMachineStates; @@ -105,7 +110,7 @@ public abstract class AbstractStateMachineFactory 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 extends LifecycleObjectS if (machine instanceof LifecycleObjectSupport) { ((LifecycleObjectSupport)machine).setAutoStartup(stateMachineConfigurationConfig.isAutoStart()); } - + // set top-level machine as relay final StateMachine fmachine = machine; fmachine.getStateMachineAccessor().doWithAllRegions(new StateMachineFunction>() { @@ -213,9 +218,24 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS public void apply(StateMachineAccess function) { function.setRelay(fmachine); } - }); + // TODO: should error out if sec is enabled but spring-security is not in cp + if (stateMachineConfigurationConfig.isSecurityEnabled()) { + final StateMachineSecurityInterceptor securityInterceptor = new StateMachineSecurityInterceptor( + stateMachineConfigurationConfig.getTransitionSecurityAccessDecisionManager(), + stateMachineConfigurationConfig.getEventSecurityAccessDecisionManager(), + stateMachineConfigurationConfig.getEventSecurityRule()); + log.info("Adding security interceptor " + securityInterceptor); + fmachine.getStateMachineAccessor().doWithAllRegions(new StateMachineFunction>() { + + @Override + public void apply(StateMachineAccess 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 extends LifecycleObjectS continue; } DefaultExternalTransition transition = new DefaultExternalTransition(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 transition = new DefaultInternalTransition(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 extends LifecycleObjectS } TreeTraverser>> traverser = new TreeTraverser>>() { - @Override - public Iterable>> children(Node> root) { - return root.getChildren(); - } + @Override + public Iterable>> children(Node> root) { + return root.getChildren(); + } }; Iterable>> postOrderTraversal = traverser.postOrderTraversal(tree.getRoot()); diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineConfigBuilder.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineConfigBuilder.java index 1373b856..97849333 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineConfigBuilder.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineConfigBuilder.java @@ -25,12 +25,15 @@ public class StateMachineConfigBuilder @SuppressWarnings("unchecked") @Override protected StateMachineConfig performBuild() throws Exception { - StateMachineTransitionBuilder sharedObject = getSharedObject(StateMachineTransitionBuilder.class); - StateMachineStateBuilder sharedObject2 = getSharedObject(StateMachineStateBuilder.class); - StateMachineConfigurationBuilder sharedObject3 = getSharedObject(StateMachineConfigurationBuilder.class); - StateMachineTransitions transitions = (StateMachineTransitions) sharedObject.build(); - StateMachineStates states = (StateMachineStates) sharedObject2.build(); - StateMachineConfigurationConfig config = (StateMachineConfigurationConfig) 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 config = (StateMachineConfigurationConfig) configurationBuilder.build(); + transitionBuilder.setSharedObject(StateMachineConfigurationConfig.class, config); + StateMachineTransitions transitions = (StateMachineTransitions) transitionBuilder.build(); + StateMachineStates states = (StateMachineStates) stateBuilder.build(); StateMachineConfig bean = new StateMachineConfig(config, transitions, states); return bean; } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineConfigurationBuilder.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineConfigurationBuilder.java index 9facc25f..9e95b469 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineConfigurationBuilder.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineConfigurationBuilder.java @@ -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 private boolean autoStart = false; private StateMachineEnsemble ensemble; private final List> listeners = new ArrayList>(); + 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 return apply(new DefaultDistributedStateMachineConfigurer()); } + @Override + public SecurityConfigurer withSecurity() throws Exception { + return apply(new DefaultSecurityConfigurer()); + } + @Override protected StateMachineConfigurationConfig performBuild() throws Exception { - return new StateMachineConfigurationConfig<>(beanFactory, taskExecutor, taskScheculer, autoStart, ensemble, listeners); + return new StateMachineConfigurationConfig(beanFactory, taskExecutor, taskScheculer, autoStart, ensemble, listeners, + securityEnabled, transitionSecurityAccessDecisionManager, eventSecurityAccessDecisionManager, eventSecurityRule, + transitionSecurityRule); } /** @@ -147,4 +163,49 @@ public class StateMachineConfigurationBuilder 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; + } + } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineConfigurationConfig.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineConfigurationConfig.java index e7d909ee..dc2ee950 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineConfigurationConfig.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineConfigurationConfig.java @@ -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 { private final boolean autoStart; private final StateMachineEnsemble ensemble; private final List> 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 { * @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 ensemble, - List> listeners) { + List> 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 { 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; + } + } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineConfigurationConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineConfigurationConfigurer.java index 7295e860..aaead955 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineConfigurationConfigurer.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineConfigurationConfigurer.java @@ -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 { */ DistributedStateMachineConfigurer withDistributed() throws Exception; + /** + * Gets a configurer for securing state machine. + * + * @return {@link SecurityConfigurer} for chaining + * @throws Exception if configuration error happens + */ + SecurityConfigurer withSecurity() throws Exception; + } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineSecurityConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineSecurityConfigurer.java new file mode 100644 index 00000000..142113b2 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineSecurityConfigurer.java @@ -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 the type of state + * @param the type of event + */ +public interface StateMachineSecurityConfigurer { + + /** + * Gets a configurer for security. + * + * @return {@link SecurityConfigurer} for chaining + * @throws Exception if configuration error happens + */ + SecurityConfigurer withSecurity() throws Exception; + +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineTransitionBuilder.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineTransitionBuilder.java index 82ad3b5f..8a22b0b4 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineTransitionBuilder.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineTransitionBuilder.java @@ -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 } public void add(S source, S target, S state, E event, Long period, Collection> actions, - Guard guard, TransitionKind kind) { - transitionData.add(new TransitionData(source, target, state, event, period, actions, guard, kind)); + Guard guard, TransitionKind kind, SecurityRule securityRule) { + // if rule not given, get it from global + if (securityRule == null) { + @SuppressWarnings("unchecked") + StateMachineConfigurationConfig config = getSharedObject(StateMachineConfigurationConfig.class); + securityRule = config.getTransitionSecurityRule(); + } + transitionData.add(new TransitionData(source, target, state, event, period, actions, guard, kind, securityRule)); } public void add(S source, List> choices) { diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineTransitions.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineTransitions.java index a67703eb..c56bd4d6 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineTransitions.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineTransitions.java @@ -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 { private final Collection> actions; private final Guard guard; private final TransitionKind kind; + private final SecurityRule securityRule; /** * Instantiates a new transition data. @@ -117,9 +119,10 @@ public class StateMachineTransitions { * @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> actions, - Guard guard, TransitionKind kind) { + Guard guard, TransitionKind kind, SecurityRule securityRule) { this.source = source; this.target = target; this.state = state; @@ -128,6 +131,7 @@ public class StateMachineTransitions { this.actions = actions; this.guard = guard; this.kind = kind; + this.securityRule = securityRule; } /** @@ -201,6 +205,15 @@ public class StateMachineTransitions { public TransitionKind getKind() { return kind; } + + /** + * Gets the security rule. + * + * @return the security rule + */ + public SecurityRule getSecurityRule() { + return securityRule; + } } /** diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/AbstractTransitionConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/AbstractTransitionConfigurer.java new file mode 100644 index 00000000..fa62f85e --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/AbstractTransitionConfigurer.java @@ -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 the type of state + * @param the type of event + */ +public abstract class AbstractTransitionConfigurer extends + AnnotationConfigurerAdapter, StateMachineTransitionConfigurer, StateMachineTransitionBuilder> { + + private S source; + private S target; + private S state; + private E event; + private Long period; + private final Collection> actions = new ArrayList>(); + private Guard 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> getActions() { + return actions; + } + + protected Guard 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 action) { + this.actions.add(action); + } + + protected void setGuard(Guard 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); + } + +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultExternalTransitionConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultExternalTransitionConfigurer.java index 5c846e3e..c4ffd218 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultExternalTransitionConfigurer.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultExternalTransitionConfigurer.java @@ -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 the type of state * @param the type of event */ -public class DefaultExternalTransitionConfigurer - extends AnnotationConfigurerAdapter, StateMachineTransitionConfigurer, StateMachineTransitionBuilder> +public class DefaultExternalTransitionConfigurer extends AbstractTransitionConfigurer implements ExternalTransitionConfigurer { - private S source; - - private S target; - - private S state; - - private E event; - - private Long period; - - private Collection> actions = new ArrayList>(); - - private Guard guard; - @Override public void configure(StateMachineTransitionBuilder 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 source(S source) { - this.source = source; + setSource(source); return this; } @Override public ExternalTransitionConfigurer target(S target) { - this.target = target; + setTarget(target); return this; } @Override public ExternalTransitionConfigurer state(S state) { - this.state = state; + setState(state); return this; } @Override public ExternalTransitionConfigurer event(E event) { - this.event = event; + setEvent(event); return this; } @Override public ExternalTransitionConfigurer timer(long period) { - this.period = period; + setPeriod(period); return this; } @Override public ExternalTransitionConfigurer action(Action action) { - actions.add(action); + addAction(action); return this; } @Override public ExternalTransitionConfigurer guard(Guard guard) { - this.guard = guard; + setGuard(guard); return this; } @@ -107,7 +88,19 @@ public class DefaultExternalTransitionConfigurer public ExternalTransitionConfigurer guardExpression(String expression) { SpelExpressionParser parser = new SpelExpressionParser( new SpelParserConfiguration(SpelCompilerMode.MIXED, null)); - this.guard = new SpelExpressionGuard(parser.parseExpression(expression)); + setGuard(new SpelExpressionGuard(parser.parseExpression(expression))); + return this; + } + + @Override + public ExternalTransitionConfigurer secured(String attributes, ComparisonType match) { + setSecurityRule(attributes, match); + return this; + } + + @Override + public ExternalTransitionConfigurer secured(String expression) { + setSecurityRule(expression); return this; } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultInternalTransitionConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultInternalTransitionConfigurer.java index c83831e2..6d707207 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultInternalTransitionConfigurer.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultInternalTransitionConfigurer.java @@ -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 the type of state * @param the type of event */ -public class DefaultInternalTransitionConfigurer - extends AnnotationConfigurerAdapter, StateMachineTransitionConfigurer, StateMachineTransitionBuilder> +public class DefaultInternalTransitionConfigurer extends AbstractTransitionConfigurer implements InternalTransitionConfigurer { - private S source; - - private S target; - - private S state; - - private E event; - - private Long period; - - private Collection> actions = new ArrayList>(); - - private Guard guard; - @Override public void configure(StateMachineTransitionBuilder 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 source(S source) { - this.source = source; + setSource(source); return this; } @Override public InternalTransitionConfigurer state(S state) { - this.state = state; + setState(state); return this; } @Override public InternalTransitionConfigurer event(E event) { - this.event = event; + setEvent(event); return this; } @Override public InternalTransitionConfigurer timer(long period) { - this.period = period; + setPeriod(period); return this; } @Override public InternalTransitionConfigurer action(Action action) { - actions.add(action); + addAction(action); return this; } @Override public InternalTransitionConfigurer guard(Guard guard) { - this.guard = guard; + setGuard(guard); return this; } @@ -101,7 +82,19 @@ public class DefaultInternalTransitionConfigurer public InternalTransitionConfigurer guardExpression(String expression) { SpelExpressionParser parser = new SpelExpressionParser( new SpelParserConfiguration(SpelCompilerMode.MIXED, null)); - this.guard = new SpelExpressionGuard(parser.parseExpression(expression)); + setGuard(new SpelExpressionGuard(parser.parseExpression(expression))); + return this; + } + + @Override + public InternalTransitionConfigurer secured(String attributes, ComparisonType match) { + setSecurityRule(attributes, match); + return this; + } + + @Override + public InternalTransitionConfigurer secured(String expression) { + setSecurityRule(expression); return this; } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultLocalTransitionConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultLocalTransitionConfigurer.java index 5cbeb4e9..e9e433a9 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultLocalTransitionConfigurer.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultLocalTransitionConfigurer.java @@ -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 the type of state * @param the type of event */ -public class DefaultLocalTransitionConfigurer - extends AnnotationConfigurerAdapter, StateMachineTransitionConfigurer, StateMachineTransitionBuilder> - implements LocalTransitionConfigurer { - - private S source; - - private S target; - - private S state; - - private E event; - - private Long period; - - private Collection> actions = new ArrayList>(); - - private Guard guard; +public class DefaultLocalTransitionConfigurer extends AbstractTransitionConfigurer implements LocalTransitionConfigurer { @Override public void configure(StateMachineTransitionBuilder 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 source(S source) { - this.source = source; + setSource(source); return this; } @Override public LocalTransitionConfigurer target(S target) { - this.target = target; + setTarget(target); return this; } @Override public LocalTransitionConfigurer state(S state) { - this.state = state; + setState(state); return this; } @Override public LocalTransitionConfigurer event(E event) { - this.event = event; + setEvent(event); return this; } @Override public LocalTransitionConfigurer timer(long period) { - this.period = period; + setPeriod(period); return this; } @Override public LocalTransitionConfigurer action(Action action) { - actions.add(action); + addAction(action); return this; } @Override public LocalTransitionConfigurer guard(Guard guard) { - this.guard = guard; + setGuard(guard); return this; } @@ -107,7 +87,19 @@ public class DefaultLocalTransitionConfigurer public LocalTransitionConfigurer guardExpression(String expression) { SpelExpressionParser parser = new SpelExpressionParser( new SpelParserConfiguration(SpelCompilerMode.MIXED, null)); - this.guard = new SpelExpressionGuard(parser.parseExpression(expression)); + setGuard(new SpelExpressionGuard(parser.parseExpression(expression))); + return this; + } + + @Override + public LocalTransitionConfigurer secured(String attributes, ComparisonType match) { + setSecurityRule(attributes, match); + return this; + } + + @Override + public LocalTransitionConfigurer secured(String expression) { + setSecurityRule(expression); return this; } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultSecurityConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultSecurityConfigurer.java new file mode 100644 index 00000000..2c4cbb2d --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultSecurityConfigurer.java @@ -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 the type of state + * @param the type of event + */ +public class DefaultSecurityConfigurer + extends AnnotationConfigurerAdapter, StateMachineConfigurationConfigurer, StateMachineConfigurationBuilder> + implements SecurityConfigurer { + + private boolean enabled = true; + private AccessDecisionManager transitionAccessDecisionManager; + private AccessDecisionManager eventAccessDecisionManager; + private SecurityRule eventSecurityRule; + private SecurityRule transitionSecurityRule; + + @Override + public void configure(StateMachineConfigurationBuilder builder) throws Exception { + if (enabled) { + builder.setSecurityEnabled(true); + builder.setTransitionSecurityAccessDecisionManager(transitionAccessDecisionManager); + builder.setEventSecurityAccessDecisionManager(eventAccessDecisionManager); + builder.setEventSecurityRule(eventSecurityRule); + builder.setTransitionSecurityRule(transitionSecurityRule); + } + } + + @Override + public SecurityConfigurer enabled(boolean enabled) { + this.enabled = enabled; + return this; + } + + @Override + public SecurityConfigurer transitionAccessDecisionManager(AccessDecisionManager accessDecisionManager) { + this.transitionAccessDecisionManager = accessDecisionManager; + return this; + } + + @Override + public SecurityConfigurer eventAccessDecisionManager(AccessDecisionManager accessDecisionManager) { + this.eventAccessDecisionManager = accessDecisionManager; + return this; + } + + @Override + public SecurityConfigurer event(String attributes, ComparisonType match) { + if (eventSecurityRule == null) { + eventSecurityRule = new SecurityRule(); + } + eventSecurityRule.setAttributes(SecurityRule.commaDelimitedListToSecurityAttributes(attributes)); + return this; + } + + @Override + public SecurityConfigurer event(String expression) { + if (eventSecurityRule == null) { + eventSecurityRule = new SecurityRule(); + } + eventSecurityRule.setExpression(expression); + return this; + } + + @Override + public SecurityConfigurer transition(String attributes, ComparisonType match) { + if (transitionSecurityRule == null) { + transitionSecurityRule = new SecurityRule(); + } + transitionSecurityRule.setAttributes(SecurityRule.commaDelimitedListToSecurityAttributes(attributes)); + return this; + } + + @Override + public SecurityConfigurer transition(String expression) { + if (transitionSecurityRule == null) { + transitionSecurityRule = new SecurityRule(); + } + transitionSecurityRule.setExpression(expression); + return this; + } + +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/ExternalTransitionConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/ExternalTransitionConfigurer.java index 9ae8df40..b65f06d9 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/ExternalTransitionConfigurer.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/ExternalTransitionConfigurer.java @@ -19,7 +19,7 @@ import org.springframework.statemachine.transition.Transition; /** * {@code TransitionConfigurer} interface for configuring external {@link Transition}s. - * + * * @author Janne Valkealahti * * @param the type of state diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/SecurityConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/SecurityConfigurer.java new file mode 100644 index 00000000..df2384da --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/SecurityConfigurer.java @@ -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 the type of state + * @param the type of event + */ +public interface SecurityConfigurer extends + AnnotationConfigurerBuilder> { + + /** + * Specify if security is enabled. On default security is enabled + * if configurer is used. + * + * @param enabled the enable flag + * @return configurer for chaining + */ + SecurityConfigurer enabled(boolean enabled); + + /** + * Specify a custom {@link AccessDecisionManager} for transitions. + * + * @param accessDecisionManager the access decision manager + * @return configurer for chaining + */ + SecurityConfigurer transitionAccessDecisionManager(AccessDecisionManager accessDecisionManager); + + /** + * Specify a custom {@link AccessDecisionManager} for events. + * + * @param accessDecisionManager the access decision manager + * @return configurer for chaining + */ + SecurityConfigurer eventAccessDecisionManager(AccessDecisionManager accessDecisionManager); + + /** + * Specify a security attributes for events. + * + * @param attributes the security attributes + * @param match the match type + * @return configurer for chaining + */ + SecurityConfigurer event(String attributes, ComparisonType match); + + /** + * Specify a security attributes for events. + * + * @param expression the the security expression + * @return configurer for chaining + */ + SecurityConfigurer event(String expression); + + /** + * Specify a security attributes for transitions. + * + * @param attributes the security attributes + * @param match the match type + * @return configurer for chaining + */ + SecurityConfigurer transition(String attributes, ComparisonType match); + + /** + * Specify a security attributes for transitions. + * + * @param expression the the security expression + * @return configurer for chaining + */ + SecurityConfigurer transition(String expression); + +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/TransitionConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/TransitionConfigurer.java index daa92fb5..ee984095 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/TransitionConfigurer.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/TransitionConfigurer.java @@ -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 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 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 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); + } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/ensemble/DistributedStateMachine.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/ensemble/DistributedStateMachine.java index 95cd3223..203c87d6 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/ensemble/DistributedStateMachine.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/ensemble/DistributedStateMachine.java @@ -185,6 +185,11 @@ public class DistributedStateMachine extends LifecycleObjectSupport implem */ private class LocalStateMachineInterceptor implements StateMachineInterceptor { + @Override + public Message preEvent(Message message, StateMachine stateMachine) { + return message; + } + @Override public void preStateChange(State state, Message message, Transition transition, StateMachine stateMachine) { diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/security/DefaultEventSecurityExpressionHandler.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/security/DefaultEventSecurityExpressionHandler.java new file mode 100644 index 00000000..7d42b537 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/security/DefaultEventSecurityExpressionHandler.java @@ -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 the type for the body of the Message + * + * @author Rob Winch + * @author Janne Valkealahti + */ +public class DefaultEventSecurityExpressionHandler extends + AbstractSecurityExpressionHandler> { + + private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); + + @Override + protected SecurityExpressionOperations createSecurityExpressionRoot( + Authentication authentication, Message 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; + } +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/security/DefaultTransitionSecurityExpressionHandler.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/security/DefaultTransitionSecurityExpressionHandler.java new file mode 100644 index 00000000..e2c2c594 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/security/DefaultTransitionSecurityExpressionHandler.java @@ -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> + implements SecurityExpressionHandler> { + + 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; + } + + /** + *

+ * 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). + *

+ * + *

+ * If null or empty, then no default role prefix is used. + *

+ * + * @param defaultRolePrefix + * the default prefix to add to roles. Default "ROLE_". + */ + public void setDefaultRolePrefix(String defaultRolePrefix) { + this.defaultRolePrefix = defaultRolePrefix; + } + +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/security/EventExpressionConfigAttribute.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/security/EventExpressionConfigAttribute.java new file mode 100644 index 00000000..8c824bc9 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/security/EventExpressionConfigAttribute.java @@ -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(); + } +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/security/EventExpressionVoter.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/security/EventExpressionVoter.java new file mode 100644 index 00000000..c2cc0f86 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/security/EventExpressionVoter.java @@ -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 implements AccessDecisionVoter> { + private SecurityExpressionHandler> expressionHandler = new DefaultEventSecurityExpressionHandler(); + + public int vote(Authentication authentication, Message message, + Collection 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 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> expressionHandler) { + Assert.notNull(expressionHandler, "expressionHandler cannot be null"); + this.expressionHandler = expressionHandler; + } +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/security/EventSecurityExpressionRoot.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/security/EventSecurityExpressionRoot.java new file mode 100644 index 00000000..e9c9eccb --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/security/EventSecurityExpressionRoot.java @@ -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; + } +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/security/EventVoter.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/security/EventVoter.java new file mode 100644 index 00000000..dae7a770 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/security/EventVoter.java @@ -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 EVENT, 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. + *

+ * All comparisons and prefixes are case sensitive. + * + * @author Janne Valkealahti + * + * @param the message type + */ +public class EventVoter implements AccessDecisionVoter>{ + + 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 event, Collection 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; + } + +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/security/SecurityRule.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/security/SecurityRule.java new file mode 100644 index 00000000..66e4c171 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/security/SecurityRule.java @@ -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 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 commaDelimitedListToSecurityAttributes(String attributes) { + Collection attrs = new HashSet(); + 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 getAttributes() { + return attributes; + } + + /** + * Sets the security attributes. + * + * @param attributes the new security attributes + */ + public void setAttributes(Collection 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 + "]"; + } + +} \ No newline at end of file diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/security/StateMachineSecurityInterceptor.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/security/StateMachineSecurityInterceptor.java new file mode 100644 index 00000000..4b97cfc8 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/security/StateMachineSecurityInterceptor.java @@ -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 the type of state + * @param the type of event + */ +public class StateMachineSecurityInterceptor extends StateMachineInterceptorAdapter { + + 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 preEvent(Message message, StateMachine stateMachine) { + if (eventSecurityRule != null) { + decide(eventSecurityRule, message); + } + return super.preEvent(message, stateMachine); + } + + @Override + public StateContext preTransition(StateContext stateContext) { + Transition 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 object) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + Collection configAttributes = getEentConfigAttributes(rule); + if (eventAccessDecisionManager != null) { + decide(eventAccessDecisionManager, authentication, object, configAttributes); + } else { + decide(createDefaultEventManager(rule), authentication, object, configAttributes); + } + } + + private void decide(SecurityRule rule, Transition object) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + Collection configAttributes = getTransitionConfigAttributes(rule); + if (transitionAccessDecisionManager != null) { + decide(transitionAccessDecisionManager, authentication, object, configAttributes); + } else { + decide(createDefaultTransitionManager(rule), authentication, object, configAttributes); + } + } + + private Collection getTransitionConfigAttributes(SecurityRule rule) { + List configAttributes = new ArrayList(); + 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 getEentConfigAttributes(SecurityRule rule) { + List configAttributes = new ArrayList(); + 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 object, + Collection configAttributes) { + if (manager.supports(object.getClass())) { + manager.decide(authentication, object, configAttributes); + } + } + + private void decide(AccessDecisionManager manager, Authentication authentication, Message object, + Collection configAttributes) { + if (manager.supports(object.getClass())) { + manager.decide(authentication, object, configAttributes); + } + } + + private AbstractAccessDecisionManager createDefaultTransitionManager(SecurityRule rule) { + List> voters = new ArrayList>(); + voters.add(new TransitionExpressionVoter()); + voters.add(new TransitionVoter()); + 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> voters = new ArrayList>(); + voters.add(new EventExpressionVoter()); + voters.add(new EventVoter()); + 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 + "]"; + } + +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/security/TransitionExpressionConfigAttribute.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/security/TransitionExpressionConfigAttribute.java new file mode 100644 index 00000000..aeec0685 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/security/TransitionExpressionConfigAttribute.java @@ -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(); + } + +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/security/TransitionExpressionVoter.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/security/TransitionExpressionVoter.java new file mode 100644 index 00000000..180b890f --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/security/TransitionExpressionVoter.java @@ -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> { + + private final SecurityExpressionHandler> 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 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 attributes) { + for (ConfigAttribute attribute : attributes) { + if (attribute instanceof TransitionExpressionConfigAttribute) { + return (TransitionExpressionConfigAttribute) attribute; + } + } + return null; + } + +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/security/TransitionSecurityExpressionRoot.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/security/TransitionSecurityExpressionRoot.java new file mode 100644 index 00000000..b439db33 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/security/TransitionSecurityExpressionRoot.java @@ -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()); + } + +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/security/TransitionVoter.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/security/TransitionVoter.java new file mode 100644 index 00000000..7a42d900 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/security/TransitionVoter.java @@ -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 + * TRANSITION_SOURCE and TRANSITION_TARGET, 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. + *

+ * All comparisons and prefixes are case sensitive. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + */ +public class TransitionVoter implements AccessDecisionVoter> { + + 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 transition, Collection 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 TRANSITION_SOURCE_ 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 TRANSITION_TARGET_ 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; + } + +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/ObjectState.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/ObjectState.java index 8d596ed9..ccf155ef 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/ObjectState.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/ObjectState.java @@ -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 extends AbstractSimpleState { + private static final Log log = LogFactory.getLog(ObjectState.class); + /** * Instantiates a new object state. * @@ -122,7 +126,11 @@ public class ObjectState extends AbstractSimpleState { Collection> actions = getExitActions(); if (actions != null) { for (Action 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 extends AbstractSimpleState { Collection> actions = getEntryActions(); if (actions != null) { for (Action action : actions) { - action.execute(context); + try { + action.execute(context); + } catch (Exception e) { + log.error("Action execution resulted error", e); + } } } } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java index 40552cf7..124ba274 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java @@ -195,6 +195,15 @@ public abstract class AbstractStateMachine 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; diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateMachineExecutor.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateMachineExecutor.java index e7a4d86b..9d226309 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateMachineExecutor.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateMachineExecutor.java @@ -190,7 +190,15 @@ public class DefaultStateMachineExecutor extends LifecycleObjectSupport im } StateContext 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; } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineInterceptor.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineInterceptor.java index 5aa6264c..e9f50804 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineInterceptor.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineInterceptor.java @@ -32,6 +32,16 @@ import org.springframework.statemachine.transition.Transition; */ public interface StateMachineInterceptor { + /** + * 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 preEvent(Message message, StateMachine stateMachine); + /** * Called prior of a state change. Throwing an exception * from this method will stop a state change logic. diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineInterceptorAdapter.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineInterceptorAdapter.java index 3e4a6619..1b6e22da 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineInterceptorAdapter.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineInterceptorAdapter.java @@ -31,6 +31,11 @@ import org.springframework.statemachine.transition.Transition; */ public class StateMachineInterceptorAdapter implements StateMachineInterceptor { + @Override + public Message preEvent(Message message, StateMachine stateMachine) { + return message; + } + @Override public void preStateChange(State state, Message message, Transition transition, StateMachine stateMachine) { diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineInterceptorList.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineInterceptorList.java index 4d2725f8..38589932 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineInterceptorList.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineInterceptorList.java @@ -71,6 +71,22 @@ public class StateMachineInterceptorList { return interceptors.remove(interceptor); } + /** + * Pre event. + * + * @param message the message + * @param stateMachine the state machine + * @return the message + */ + public Message preEvent(Message message, StateMachine stateMachine) { + for (StateMachineInterceptor interceptor : interceptors) { + if ((message = interceptor.preEvent(message, stateMachine)) == null) { + break; + } + } + return message; + } + /** * Pre state change. * diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/AbstractExternalTransition.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/AbstractExternalTransition.java index 83f9eb95..d7f01a81 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/AbstractExternalTransition.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/AbstractExternalTransition.java @@ -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 extends AbstractTransition implements Transition { - public AbstractExternalTransition(State source, State target, Collection> actions, E event, Guard guard, Trigger trigger) { + public AbstractExternalTransition(State source, State target, Collection> actions, + E event, Guard guard, Trigger trigger, SecurityRule securityRule) { + super(source, target, actions, event, TransitionKind.EXTERNAL, guard, trigger, securityRule); + } + + public AbstractExternalTransition(State source, State target, Collection> actions, + E event, Guard guard, Trigger trigger) { super(source, target, actions, event, TransitionKind.EXTERNAL, guard, trigger); } - } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/AbstractInternalTransition.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/AbstractInternalTransition.java index 8eb38d2e..355a989c 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/AbstractInternalTransition.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/AbstractInternalTransition.java @@ -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 extends AbstractTransition implements Transition { +public class AbstractInternalTransition extends AbstractTransition implements Transition { - public AbstractInternalTransition(State source,Collection> actions, E event, Guard guard, Trigger trigger) { + public AbstractInternalTransition(State source, Collection> actions, E event, Guard guard, + Trigger trigger) { super(source, source, actions, event, TransitionKind.INTERNAL, guard, trigger); } + public AbstractInternalTransition(State source, Collection> actions, E event, Guard guard, + Trigger trigger, SecurityRule securityRule) { + super(source, source, actions, event, TransitionKind.INTERNAL, guard, trigger, securityRule); + } + } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/AbstractLocalTransition.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/AbstractLocalTransition.java index ab0eb881..6082123a 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/AbstractLocalTransition.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/AbstractLocalTransition.java @@ -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 extends AbstractTransition implements Transition { +public class AbstractLocalTransition extends AbstractTransition implements Transition { - public AbstractLocalTransition(State source, State target,Collection> actions, E event, Guard guard, Trigger trigger) { + public AbstractLocalTransition(State source, State target, Collection> actions, E event, + Guard guard, Trigger trigger) { super(source, target, actions, event, TransitionKind.LOCAL, guard, trigger); } + public AbstractLocalTransition(State source, State target, Collection> actions, E event, + Guard guard, Trigger trigger, SecurityRule securityRule) { + super(source, target, actions, event, TransitionKind.LOCAL, guard, trigger, securityRule); + } + } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/AbstractTransition.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/AbstractTransition.java index 03128c97..900e3e8d 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/AbstractTransition.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/AbstractTransition.java @@ -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 implements Transition { private final Guard guard; - private Trigger trigger; + private final Trigger trigger; + + private final SecurityRule securityRule; public AbstractTransition(State source, State target, Collection> actions, E event, TransitionKind kind, Guard guard, Trigger trigger) { + this(source, target, actions, event, kind, guard, trigger, null); + } + + public AbstractTransition(State source, State target, Collection> actions, E event, + TransitionKind kind, Guard guard, Trigger 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 implements Transition { this.kind = kind; this.guard = guard; this.trigger = trigger; + this.securityRule = securityRule; } @Override @@ -98,4 +107,9 @@ public abstract class AbstractTransition implements Transition { return kind; } + @Override + public SecurityRule getSecurityRule() { + return securityRule; + } + } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/DefaultExternalTransition.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/DefaultExternalTransition.java index 4726625c..71495a32 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/DefaultExternalTransition.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/DefaultExternalTransition.java @@ -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 extends AbstractExternalTransition { - public DefaultExternalTransition(State source, State target, Collection> actions, E event, Guard guard, Trigger trigger) { + public DefaultExternalTransition(State source, State target, Collection> actions, E event, + Guard guard, Trigger trigger) { super(source, target, actions, event, guard, trigger); } + public DefaultExternalTransition(State source, State target, Collection> actions, E event, + Guard guard, Trigger trigger, SecurityRule securityRule) { + super(source, target, actions, event, guard, trigger, securityRule); + } + } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/DefaultInternalTransition.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/DefaultInternalTransition.java index 538144e5..5f9962dc 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/DefaultInternalTransition.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/DefaultInternalTransition.java @@ -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 extends AbstractInternalTransition { - public DefaultInternalTransition(State source, Collection> actions, E event, Guard guard, Trigger trigger) { + public DefaultInternalTransition(State source, Collection> actions, E event, Guard guard, + Trigger trigger) { super(source, actions, event, guard, trigger); } + public DefaultInternalTransition(State source, Collection> actions, E event, Guard guard, + Trigger trigger, SecurityRule securityRule) { + super(source, actions, event, guard, trigger, securityRule); + } + } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/InitialTransition.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/InitialTransition.java index 46c19afb..0fa72e9e 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/InitialTransition.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/InitialTransition.java @@ -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 implements Transition { return TransitionKind.INITIAL; } + @Override + public SecurityRule getSecurityRule() { + // initial cannot have security + return null; + } + } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/Transition.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/Transition.java index 37051ac3..efacddb3 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/Transition.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/Transition.java @@ -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 the type of state * @param the type of event */ @@ -40,7 +41,7 @@ public interface Transition { * @return true, if transition happened, false otherwise */ boolean transit(StateContext context); - + /** * Gets the source state of this transition. * @@ -76,4 +77,11 @@ public interface Transition { */ TransitionKind getKind(); + /** + * Gets the security rule. + * + * @return the security rule + */ + SecurityRule getSecurityRule(); + } diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/docs/DocsConfigurationSampleTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/docs/DocsConfigurationSampleTests.java index edd4fa07..879a7729 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/docs/DocsConfigurationSampleTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/docs/DocsConfigurationSampleTests.java @@ -956,6 +956,11 @@ public class DocsConfigurationSampleTests extends AbstractStateMachineTests { stateMachine.getStateMachineAccessor() .withRegion().addStateMachineInterceptor(new StateMachineInterceptor() { + @Override + public Message preEvent(Message message, StateMachine stateMachine) { + return message; + } + @Override public StateContext preTransition(StateContext stateContext) { return stateContext; @@ -1021,7 +1026,7 @@ public class DocsConfigurationSampleTests extends AbstractStateMachineTests { @Override public void stateMachineError(StateMachine 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 { - @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[] diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/docs/DocsConfigurationSampleTests3.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/docs/DocsConfigurationSampleTests3.java new file mode 100644 index 00000000..dfa3dbb7 --- /dev/null +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/docs/DocsConfigurationSampleTests3.java @@ -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 { + + @Override + public void configure(StateMachineConfigurationConfigurer 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 { + + @Override + public void configure(StateMachineTransitionConfigurer 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 { + + @Override + public void configure(StateMachineConfigurationConfigurer config) + throws Exception { + config + .withSecurity() + .enabled(true); + } + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial("S0") + .state("S1"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source("S0") + .target("S1") + .action(securedAction()) + .event("A"); + } + + @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS) + @Bean + public Action securedAction() { + return new Action() { + + @Secured("ROLE_ANONYMOUS") + @Override + public void execute(StateContext context) { + } + }; + } + + } +// end::snippetC[] + +// tag::snippetD[] + @Configuration + @EnableStateMachine + static class Config4 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineConfigurationConfigurer 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 { + + @Override + public void configure(StateMachineConfigurationConfigurer config) + throws Exception { + config + .withSecurity() + .enabled(true) + .transition("true") + .transition("ROLE_ANONYMOUS", ComparisonType.ANY); + } + } +// end::snippetF[] + +} diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/security/AbstractSecurityTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/security/AbstractSecurityTests.java new file mode 100644 index 00000000..d9f1bdbe --- /dev/null +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/security/AbstractSecurityTests.java @@ -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 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 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 { + + volatile CountDownLatch stateChangedLatch = new CountDownLatch(1); + volatile int stateChangedCount = 0; + + @Override + public void stateChanged(State from, State to) { + stateChangedCount++; + stateChangedLatch.countDown(); + } + + public void reset(int c1) { + stateChangedLatch = new CountDownLatch(c1); + stateChangedCount = 0; + } + + } + + protected static StateMachine buildMachine(TestListener listener, String attributes, ComparisonType match, String expression) throws Exception { + return buildMachine(listener, attributes, match, expression, null, null, null); + } + + protected static StateMachine buildMachine(TestListener listener, String attributes, ComparisonType match, + String expression, String eventAttributes, ComparisonType eventMatch, String eventExpression) throws Exception { + Builder builder = StateMachineBuilder.builder(); + + StateMachineConfigurationConfigurer configureConfiguration = builder.configureConfiguration(); + configureConfiguration.withConfiguration() + .listener(listener) + .autoStartup(true) + .taskExecutor(new SyncTaskExecutor()); + + SecurityConfigurer 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 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(); + } + +} diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/security/ActionSecurityTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/security/ActionSecurityTests.java new file mode 100644 index 00000000..6daf4347 --- /dev/null +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/security/ActionSecurityTests.java @@ -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 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 { + + @Override + public void configure(StateMachineConfigurationConfigurer config) + throws Exception { + config + .withConfiguration() + .listener(testListener()) + .autoStartup(true) + .and() + .withSecurity() + .enabled(true); + } + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial(States.S0) + .state(States.S0) + .state(States.S1, action1(), null); + } + + @Override + public void configure(StateMachineTransitionConfigurer 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 { + + int count; + + @Secured("ROLE_USER") + @Override + public void execute(StateContext context) { + count++; + } + + public int getCount() { + return count; + } + } + + private static class TestListener extends StateMachineListenerAdapter { + + volatile CountDownLatch stateChangedLatch = new CountDownLatch(1); + volatile int stateChangedCount = 0; + + @Override + public void stateChanged(State from, State to) { + stateChangedCount++; + stateChangedLatch.countDown(); + } + + public void reset(int c1) { + stateChangedLatch = new CountDownLatch(c1); + stateChangedCount = 0; + } + + } + +} diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/security/EventSecurityTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/security/EventSecurityTests.java new file mode 100644 index 00000000..ea8b4e11 --- /dev/null +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/security/EventSecurityTests.java @@ -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 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 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 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 machine = buildMachine(listener, null, null, null, "EVENT_A", ComparisonType.ALL, null); + assertTransitionAllowed(machine, listener); + } + + @Configuration + public static class Config { + } + +} diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/security/SecurityConfigTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/security/SecurityConfigTests.java new file mode 100644 index 00000000..e19eeef6 --- /dev/null +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/security/SecurityConfigTests.java @@ -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 machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); + assertThat(machine, notNullValue()); + + StateMachineInterceptorList ilist = TestUtils.readField("interceptors", machine); + List> 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 machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); + assertThat(machine, notNullValue()); + + StateMachineInterceptorList ilist = TestUtils.readField("interceptors", machine); + List> 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 machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); + assertThat(machine, notNullValue()); + + StateMachineInterceptorList ilist = TestUtils.readField("interceptors", machine); + List> 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 machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); + assertThat(machine, notNullValue()); + + StateMachineInterceptorList ilist = TestUtils.readField("interceptors", machine); + List> 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 machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); + assertThat(machine, notNullValue()); + + StateMachineInterceptorList ilist = TestUtils.readField("interceptors", machine); + List> 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 machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); + assertThat(machine, notNullValue()); + + Transition 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 machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); + assertThat(machine, notNullValue()); + + Transition 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 machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); + assertThat(machine, notNullValue()); + + StateMachineInterceptorList ilist = TestUtils.readField("interceptors", machine); + List> 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 { + + @Override + public void configure(StateMachineConfigurationConfigurer config) + throws Exception { + config + .withSecurity() + .enabled(true); + } + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial("S0") + .state("S1"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source("S0") + .target("S1") + .event("A"); + } + + } + + @Configuration + @EnableStateMachine + static class Config2 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineConfigurationConfigurer config) + throws Exception { + config + .withSecurity() + .enabled(false); + } + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial("S0") + .state("S1"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source("S0") + .target("S1") + .event("A"); + } + + } + + @Configuration + @EnableStateMachine + static class Config3 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineConfigurationConfigurer config) + throws Exception { + config + .withSecurity(); + } + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial("S0") + .state("S1"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source("S0") + .target("S1") + .event("A"); + } + + } + + @Configuration + @EnableStateMachine + static class Config4 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial("S0") + .state("S1"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source("S0") + .target("S1") + .event("A"); + } + + } + + @Configuration + @EnableStateMachine + static class Config5 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineConfigurationConfigurer config) + throws Exception { + config + .withSecurity() + .eventAccessDecisionManager(new MockAccessDecisionManager()) + .transitionAccessDecisionManager(new MockAccessDecisionManager()); + } + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial("S0") + .state("S1"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source("S0") + .target("S1") + .event("A"); + } + + } + + @Configuration + @EnableStateMachine + static class Config6 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineConfigurationConfigurer config) + throws Exception { + config + .withSecurity() + .enabled(true); + } + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial("S0") + .state("S1"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source("S0") + .target("S1") + .event("A") + .secured("expression") + .secured("FOO", ComparisonType.ALL); + } + + } + + @Configuration + @EnableStateMachine + static class Config7 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineConfigurationConfigurer config) + throws Exception { + config + .withSecurity() + .enabled(true) + .event("expression") + .event("FOO", ComparisonType.ALL); + } + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial("S0") + .state("S1"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source("S0") + .target("S1") + .event("A"); + } + + } + + @Configuration + @EnableStateMachine + static class Config8 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineConfigurationConfigurer config) + throws Exception { + config + .withSecurity() + .enabled(true) + .transition("expression") + .transition("FOO", ComparisonType.ALL); + } + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial("S0") + .state("S1"); + } + + @Override + public void configure(StateMachineTransitionConfigurer 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 configAttributes) + throws AccessDeniedException, InsufficientAuthenticationException { + } + + @Override + public boolean supports(ConfigAttribute attribute) { + return false; + } + + @Override + public boolean supports(Class clazz) { + return false; + } + } + +} diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/security/SecurityRuleTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/security/SecurityRuleTests.java new file mode 100644 index 00000000..b8eacb90 --- /dev/null +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/security/SecurityRuleTests.java @@ -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 attributes = new ArrayList(); + attributes.add("ROLE_1"); + attributes.add("ROLE_2"); + assertEquals("ROLE_1, ROLE_2", SecurityRule.securityAttributesToCommaDelimitedList(attributes)); + } + + @Test + public void testConvertAttributesFromCommaSeparatedString() { + Collection 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); + } + +} diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/security/TransitionSecurityAttributeTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/security/TransitionSecurityAttributeTests.java new file mode 100644 index 00000000..0d8f9953 --- /dev/null +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/security/TransitionSecurityAttributeTests.java @@ -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 machine = buildMachine(listener, "ROLE_ANONYMOUS", ComparisonType.ANY, null); + assertTransitionDenied(machine, listener); + } + + @Test + @WithMockUser + public void testTransitionSource() throws Exception { + TestListener listener = new TestListener(); + StateMachine machine = buildMachine(listener, "TRANSITION_SOURCE_S0", ComparisonType.ANY, null); + assertTransitionAllowed(machine, listener); + } + + @Test + @WithMockUser + public void testTransitionTarget() throws Exception { + TestListener listener = new TestListener(); + StateMachine machine = buildMachine(listener, "TRANSITION_TARGET_S1", ComparisonType.ANY, null); + assertTransitionAllowed(machine, listener); + } + + @Test + @WithMockUser + public void testTransitionSourceAndTargetAll() throws Exception { + TestListener listener = new TestListener(); + StateMachine 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 machine = buildMachine(listener, "TRANSITION_SOURCE_S0,TRANSITION_TARGET_S1", ComparisonType.ANY, null); + assertTransitionAllowed(machine, listener); + } + + @Configuration + public static class Config { + } + +} diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/security/TransitionSecurityExpressionRootTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/security/TransitionSecurityExpressionRootTests.java new file mode 100644 index 00000000..1768feed --- /dev/null +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/security/TransitionSecurityExpressionRootTests.java @@ -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)); + } + +} diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/security/TransitionSecurityExpressionTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/security/TransitionSecurityExpressionTests.java new file mode 100644 index 00000000..8e02a218 --- /dev/null +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/security/TransitionSecurityExpressionTests.java @@ -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 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 machine = buildMachine(listener, null, null, "hasRole('FOO')"); + assertTransitionAllowed(machine, listener); + } + + private static void assertTransitionAllowed(StateMachine 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 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 { + + volatile CountDownLatch stateChangedLatch = new CountDownLatch(1); + volatile int stateChangedCount = 0; + + @Override + public void stateChanged(State from, State to) { + stateChangedCount++; + stateChangedLatch.countDown(); + } + + public void reset(int c1) { + stateChangedLatch = new CountDownLatch(c1); + stateChangedCount = 0; + } + + } + + private static StateMachine buildMachine(TestListener listener, String attributes, ComparisonType match, String expression) throws Exception { + Builder builder = StateMachineBuilder.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 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(); + } + +} diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/security/TransitionSecurityTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/security/TransitionSecurityTests.java new file mode 100644 index 00000000..ad1c226f --- /dev/null +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/security/TransitionSecurityTests.java @@ -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 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 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 machine = buildMachine(listener, "ROLE_ANONYMOUS", ComparisonType.ANY, null); + assertTransitionDenied(machine, listener); + } + + @Test + @WithMockUser + public void testExpression() throws Exception { + TestListener listener = new TestListener(); + StateMachine machine = buildMachine(listener, null, null, "hasTarget('S1')"); + assertTransitionDenied(machine, listener); + } + + @Configuration + public static class Config { + } + +} diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/support/StateChangeInterceptorTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/support/StateChangeInterceptorTests.java index 8527b398..c851eff7 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/support/StateChangeInterceptorTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/support/StateChangeInterceptorTests.java @@ -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 { @@ -272,6 +272,11 @@ public class StateChangeInterceptorTests extends AbstractStateMachineTests { volatile CountDownLatch preStateChangeLatch = new CountDownLatch(1); volatile int preStateChangeCount = 0; + @Override + public Message preEvent(Message message, StateMachine stateMachine) { + return message; + } + @Override public void preStateChange(State state, Message message, Transition transition, StateMachine stateMachine) { diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/support/StateContextExpressionMethodsTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/support/StateContextExpressionMethodsTests.java index 9fea64ac..e5c70a3b 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/support/StateContextExpressionMethodsTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/support/StateContextExpressionMethodsTests.java @@ -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 { diff --git a/spring-statemachine-samples/build.gradle b/spring-statemachine-samples/build.gradle index b7d6585e..a3340fda 100644 --- a/spring-statemachine-samples/build.gradle +++ b/spring-statemachine-samples/build.gradle @@ -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") + } +} diff --git a/spring-statemachine-samples/security/src/main/java/demo/security/Application.java b/spring-statemachine-samples/security/src/main/java/demo/security/Application.java new file mode 100644 index 00000000..1ea681f8 --- /dev/null +++ b/spring-statemachine-samples/security/src/main/java/demo/security/Application.java @@ -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); + } +} diff --git a/spring-statemachine-samples/security/src/main/java/demo/security/StateMachineConfig.java b/spring-statemachine-samples/security/src/main/java/demo/security/StateMachineConfig.java new file mode 100644 index 00000000..0d1e5a5f --- /dev/null +++ b/spring-statemachine-samples/security/src/main/java/demo/security/StateMachineConfig.java @@ -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 { + +//tag::snippetA[] + @Override + public void configure(StateMachineConfigurationConfigurer config) + throws Exception { + config + .withConfiguration() + .autoStartup(true) + .and() + .withSecurity() + .enabled(true) + .event("hasRole('USER')"); + } +//end::snippetA[] + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial(States.S0) + .states(EnumSet.allOf(States.class)); + } + +//tag::snippetB[] + @Override + public void configure(StateMachineTransitionConfigurer 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 adminAction() { + return new Action() { + + @Secured("ROLE_ADMIN") + @Override + public void execute(StateContext context) { + log.info("Executed only for admin role"); + } + }; + } +//end::snippetC[] + +//tag::snippetD[] + @Bean + public Action transitionAction() { + return new Action() { + + @Override + public void execute(StateContext 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; + } + +} diff --git a/spring-statemachine-samples/security/src/main/java/demo/security/StateMachineController.java b/spring-statemachine-samples/security/src/main/java/demo/security/StateMachineController.java new file mode 100644 index 00000000..cef6e806 --- /dev/null +++ b/spring-statemachine-samples/security/src/main/java/demo/security/StateMachineController.java @@ -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 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"; + } + +} \ No newline at end of file diff --git a/spring-statemachine-samples/security/src/main/resources/application.yml b/spring-statemachine-samples/security/src/main/resources/application.yml new file mode 100644 index 00000000..ec82d6ae --- /dev/null +++ b/spring-statemachine-samples/security/src/main/resources/application.yml @@ -0,0 +1,3 @@ +security: + basic: + enabled: false \ No newline at end of file diff --git a/spring-statemachine-samples/security/src/main/resources/logback.xml b/spring-statemachine-samples/security/src/main/resources/logback.xml new file mode 100644 index 00000000..7d5b0732 --- /dev/null +++ b/spring-statemachine-samples/security/src/main/resources/logback.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/spring-statemachine-samples/security/src/main/resources/statechartmodel.txt b/spring-statemachine-samples/security/src/main/resources/statechartmodel.txt new file mode 100644 index 00000000..b1080936 --- /dev/null +++ b/spring-statemachine-samples/security/src/main/resources/statechartmodel.txt @@ -0,0 +1,19 @@ ++--------------------------------------------------------------------------+ +| SM | ++--------------------------------------------------------------------------+ +| | +| +-----------+ A +-----------+ B +--------+ E +--------+ | +| *-->| S0 |------>| S1 |------>| S2 |------>| S3 | | +| | | | | | | | | | +| | D | | F | | | | | | +| | +-------+ | | +-------+ | | | | | | +| | | | | | | | | | | | | | +| | | v | | | v | | | | | | +| +-----------+ +-----------+ +--------+ +--------+ | +| ^ ^ | | | +| | | C | | | +| | +------------------------------------+ | | +| | C | | +| +----------------------------------------------------------+ | +| | ++--------------------------------------------------------------------------+ diff --git a/spring-statemachine-samples/security/src/main/resources/templates/states.html b/spring-statemachine-samples/security/src/main/resources/templates/states.html new file mode 100644 index 00000000..c6ac3605 --- /dev/null +++ b/spring-statemachine-samples/security/src/main/resources/templates/states.html @@ -0,0 +1,27 @@ + + + + Spring Statemachine Scope Demo + + + +

+
+
+ +
+

+

+
+

+

+ + + + + + +
+
+
+