diff --git a/common-build/ivysettings.xml b/common-build/ivysettings.xml
index 8cba4721..7a2d9ca3 100644
--- a/common-build/ivysettings.xml
+++ b/common-build/ivysettings.xml
@@ -95,5 +95,6 @@
+
diff --git a/spring-webflow/.classpath b/spring-webflow/.classpath
index 82077eaa..bb2c7e52 100644
--- a/spring-webflow/.classpath
+++ b/spring-webflow/.classpath
@@ -47,5 +47,6 @@
+
diff --git a/spring-webflow/ivy.xml b/spring-webflow/ivy.xml
index 6064d458..557d8eb5 100644
--- a/spring-webflow/ivy.xml
+++ b/spring-webflow/ivy.xml
@@ -56,6 +56,7 @@
+
diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/XmlFlowBuilder.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/XmlFlowBuilder.java
index 1eb0714f..de6daf95 100644
--- a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/XmlFlowBuilder.java
+++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/XmlFlowBuilder.java
@@ -85,6 +85,7 @@ import org.springframework.webflow.execution.Action;
import org.springframework.webflow.execution.RequestContext;
import org.springframework.webflow.execution.ScopeType;
import org.springframework.webflow.execution.ViewFactory;
+import org.springframework.webflow.security.SecurityRule;
import org.springframework.webflow.util.ResourceHolder;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
@@ -116,6 +117,7 @@ import org.xml.sax.SAXException;
*
* @author Erwin Vervaet
* @author Keith Donald
+ * @author Scott Andrews
*/
public class XmlFlowBuilder extends AbstractFlowBuilder implements ResourceHolder {
@@ -237,6 +239,12 @@ public class XmlFlowBuilder extends AbstractFlowBuilder implements ResourceHolde
private static final String VIEW_ATTRIBUTE = "view";
+ private static final String SECURED_ELEMENT = "secured";
+
+ private static final String AUTHORITIES_ATTRIBUTE = "authorities";
+
+ private static final String MATCH_ATTRIBUTE = "match";
+
/**
* Prefix used when the encoded view name wants to specify that a redirect to an external URL is required.
* ("externalRedirect:")
@@ -407,6 +415,7 @@ public class XmlFlowBuilder extends AbstractFlowBuilder implements ResourceHolde
AttributeMap externallyAssignedAttributes = getLocalContext().getFlowAttributes();
MutableAttributeMap flowAttributes = parseAttributes(flowElement);
parseAndSetPersistenceContextAttribute(flowElement, flowAttributes);
+ parseAndSetSecuredAttribute(flowElement, flowAttributes);
return getFlowArtifactFactory().createFlow(flowId, flowAttributes.union(externallyAssignedAttributes));
}
@@ -555,9 +564,11 @@ public class XmlFlowBuilder extends AbstractFlowBuilder implements ResourceHolde
}
private void parseAndAddActionState(Element element, Flow flow) {
+ MutableAttributeMap attributes = parseAttributes(element);
+ parseAndSetSecuredAttribute(element, attributes);
getFlowArtifactFactory().createActionState(parseId(element), flow, parseEntryActions(element),
parseAnnotatedActions(element), parseTransitions(element), parseExceptionHandlers(element),
- parseExitActions(element), parseAttributes(element));
+ parseExitActions(element), attributes);
}
private void parseAndAddViewState(Element element, Flow flow) {
@@ -570,28 +581,34 @@ public class XmlFlowBuilder extends AbstractFlowBuilder implements ResourceHolde
if (element.hasAttribute("popup")) {
popup = ((Boolean) fromStringTo(Boolean.class).execute(element.getAttribute("popup"))).booleanValue();
}
+ MutableAttributeMap attributes = parseAttributes(element);
+ parseAndSetSecuredAttribute(element, attributes);
getFlowArtifactFactory().createViewState(parseId(element), flow, parseViewVariables(element),
parseEntryActions(element), viewFactory, redirect, popup, parseRenderActions(element),
- parseTransitions(element), parseExceptionHandlers(element), parseExitActions(element),
- parseAttributes(element));
+ parseTransitions(element), parseExceptionHandlers(element), parseExitActions(element), attributes);
}
private void parseAndAddDecisionState(Element element, Flow flow) {
- getFlowArtifactFactory()
- .createDecisionState(parseId(element), flow, parseEntryActions(element), parseIfs(element),
- parseExceptionHandlers(element), parseExitActions(element), parseAttributes(element));
+ MutableAttributeMap attributes = parseAttributes(element);
+ parseAndSetSecuredAttribute(element, attributes);
+ getFlowArtifactFactory().createDecisionState(parseId(element), flow, parseEntryActions(element),
+ parseIfs(element), parseExceptionHandlers(element), parseExitActions(element), attributes);
}
private void parseAndAddSubflowState(Element element, Flow flow) {
+ MutableAttributeMap attributes = parseAttributes(element);
+ parseAndSetSecuredAttribute(element, attributes);
getFlowArtifactFactory().createSubflowState(parseId(element), flow, parseEntryActions(element),
parseSubflow(element), parseFlowAttributeMapper(element), parseTransitions(element),
- parseExceptionHandlers(element), parseExitActions(element), parseAttributes(element));
+ parseExceptionHandlers(element), parseExitActions(element), attributes);
}
private void parseAndAddEndState(Element element, Flow flow) {
+ MutableAttributeMap attributes = parseAttributes(element);
+ parseAndSetSecuredAttribute(element, attributes);
getFlowArtifactFactory().createEndState(parseId(element), flow, parseEntryActions(element),
parseFinalResponseAction(element), parseOutputMapper(element), parseExceptionHandlers(element),
- parseAttributes(element));
+ attributes);
}
private String parseId(Element element) {
@@ -691,8 +708,10 @@ public class XmlFlowBuilder extends AbstractFlowBuilder implements ResourceHolde
TargetStateResolver targetStateResolver = (TargetStateResolver) fromStringTo(TargetStateResolver.class)
.execute(element.getAttribute(TO_ATTRIBUTE));
TransitionCriteria executionCriteria = TransitionCriteriaChain.criteriaChainFor(parseAnnotatedActions(element));
+ MutableAttributeMap attributes = parseAttributes(element);
+ parseAndSetSecuredAttribute(element, attributes);
return getFlowArtifactFactory().createTransition(targetStateResolver, matchingCriteria, executionCriteria,
- parseAttributes(element));
+ attributes);
}
private Flow parseSubflow(Element element) {
@@ -1133,6 +1152,25 @@ public class XmlFlowBuilder extends AbstractFlowBuilder implements ResourceHolde
element.getAttribute(BEAN_ATTRIBUTE), FlowExecutionExceptionHandler.class);
}
+ private void parseAndSetSecuredAttribute(Element element, MutableAttributeMap attributes) {
+ Element secured = DomUtils.getChildElementByTagName(element, SECURED_ELEMENT);
+ if (secured != null) {
+ SecurityRule rule = new SecurityRule();
+ rule.setRequiredAuthorities(SecurityRule.convertAuthoritiesFromCommaSeparatedString(secured
+ .getAttribute(AUTHORITIES_ATTRIBUTE)));
+ String comparisonType = secured.getAttribute(MATCH_ATTRIBUTE);
+ if ("any".equals(comparisonType)) {
+ rule.setComparisonType(SecurityRule.COMPARISON_ANY);
+ } else if ("all".equals(comparisonType)) {
+ rule.setComparisonType(SecurityRule.COMPARISON_ALL);
+ } else {
+ // default to any
+ rule.setComparisonType(SecurityRule.COMPARISON_ANY);
+ }
+ attributes.put(SecurityRule.SECURITY_AUTHORITY_ATTRIBUTE_NAME, rule);
+ }
+ }
+
private ConversionExecutor fromStringTo(Class targetType) throws ConversionException {
return getLocalContext().getConversionService().getConversionExecutor(String.class, targetType);
}
diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/spring-webflow-2.0.xsd b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/spring-webflow-2.0.xsd
index df534a93..a8dda316 100644
--- a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/spring-webflow-2.0.xsd
+++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/spring-webflow-2.0.xsd
@@ -202,6 +202,9 @@ A flow may also exhibit the following characteristics:
Be annotated with attributes that define descriptive properties that may affect flow execution.
(See the <attribute/> element)
+Secured flow
+(See the <secured/> element)
+
Be a persistence context for managing persistent objects during the course of flow execution.
(See the <persistence-context/> element)
@@ -241,6 +244,15 @@ An attribute describing this flow.
]]>
+
+
+
+
+
+
+
@@ -673,6 +685,15 @@ An attribute describing this state.
]]>
+
+
+
+
+
+
+
@@ -1218,6 +1239,15 @@ An attribute describing this transition.
]]>
+
+
+
+
+
+
+
@@ -1295,6 +1325,15 @@ An attribute describing this state.
+
+
+
+
+
+
+
@@ -1304,7 +1343,7 @@ destroyed when the view-state exits. A view variable is stored in flow scope.
]]>
-
+
@@ -1455,6 +1494,15 @@ An attribute describing this state.
]]>
+
+
+
+
+
+
+
@@ -1572,6 +1620,15 @@ An attribute describing this state.
]]>
+
+
+
+
+
+
+
@@ -1735,6 +1792,15 @@ An attribute describing this state.
]]>
+
+
+
+
+
+
+
@@ -1914,5 +1980,58 @@ For example:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/spring-webflow/src/main/java/org/springframework/webflow/security/SecurityFlowExecutionListener.java b/spring-webflow/src/main/java/org/springframework/webflow/security/SecurityFlowExecutionListener.java
new file mode 100644
index 00000000..0900cd09
--- /dev/null
+++ b/spring-webflow/src/main/java/org/springframework/webflow/security/SecurityFlowExecutionListener.java
@@ -0,0 +1,84 @@
+package org.springframework.webflow.security;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+
+import org.springframework.security.AccessDeniedException;
+import org.springframework.security.Authentication;
+import org.springframework.security.context.SecurityContextHolder;
+import org.springframework.webflow.definition.FlowDefinition;
+import org.springframework.webflow.definition.StateDefinition;
+import org.springframework.webflow.definition.TransitionDefinition;
+import org.springframework.webflow.execution.EnterStateVetoException;
+import org.springframework.webflow.execution.FlowExecutionListenerAdapter;
+import org.springframework.webflow.execution.RequestContext;
+
+/**
+ * Flow security integration with Spring Security
+ *
+ * @author Scott Andrews
+ */
+public class SecurityFlowExecutionListener extends FlowExecutionListenerAdapter {
+
+ /**
+ * Check security authorization when flow session starts
+ */
+ public void sessionCreating(RequestContext context, FlowDefinition definition) {
+ SecurityRule rule = (SecurityRule) definition.getAttributes().get(
+ SecurityRule.SECURITY_AUTHORITY_ATTRIBUTE_NAME);
+ if (rule != null) {
+ Collection principalAuthorities = getPrincipalAuthorities();
+ if (!rule.isAuthorized(principalAuthorities)) {
+ throw new AccessDeniedException("Required authority not found: "
+ + SecurityRule.convertAuthoritiesToCommaSeparatedString(rule
+ .getNonGrantedAuthorities(principalAuthorities)));
+ }
+ }
+ }
+
+ /**
+ * Check security authorization when entering state
+ */
+ public void stateEntering(RequestContext context, StateDefinition state) throws EnterStateVetoException {
+ SecurityRule rule = (SecurityRule) state.getAttributes().get(SecurityRule.SECURITY_AUTHORITY_ATTRIBUTE_NAME);
+ if (rule != null) {
+ Collection principalAuthorities = getPrincipalAuthorities();
+ if (!rule.isAuthorized(principalAuthorities)) {
+ throw new AccessDeniedException("Required authority not found: "
+ + SecurityRule.convertAuthoritiesToCommaSeparatedString(rule
+ .getNonGrantedAuthorities(principalAuthorities)));
+ }
+ }
+ }
+
+ /**
+ * Check security authorization on transition
+ */
+ public void transitionExecuting(RequestContext context, TransitionDefinition transition) {
+ SecurityRule rule = (SecurityRule) transition.getAttributes().get(
+ SecurityRule.SECURITY_AUTHORITY_ATTRIBUTE_NAME);
+ if (rule != null) {
+ Collection principalAuthorities = getPrincipalAuthorities();
+ if (!rule.isAuthorized(principalAuthorities)) {
+ throw new AccessDeniedException("Required authority not found: "
+ + SecurityRule.convertAuthoritiesToCommaSeparatedString(rule
+ .getNonGrantedAuthorities(principalAuthorities)));
+ }
+ }
+ }
+
+ /**
+ * Get Spring Security authorities for the principal
+ * @return granted authorities for the principal
+ */
+ protected Collection getPrincipalAuthorities() {
+ Authentication currentUser = SecurityContextHolder.getContext().getAuthentication();
+ if ((null == currentUser) || (null == currentUser.getAuthorities())
+ || (currentUser.getAuthorities().length < 1)) {
+ return Collections.EMPTY_LIST;
+ }
+ return Arrays.asList(currentUser.getAuthorities());
+ }
+
+}
diff --git a/spring-webflow/src/main/java/org/springframework/webflow/security/SecurityRule.java b/spring-webflow/src/main/java/org/springframework/webflow/security/SecurityRule.java
new file mode 100644
index 00000000..0409625d
--- /dev/null
+++ b/spring-webflow/src/main/java/org/springframework/webflow/security/SecurityRule.java
@@ -0,0 +1,155 @@
+package org.springframework.webflow.security;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+
+/**
+ * Encapsulates the rules for comparing security authorities
+ *
+ * @author Scott Andrews
+ */
+public class SecurityRule {
+
+ /**
+ * Attribute name for the location of the security rule
+ */
+ public static final String SECURITY_AUTHORITY_ATTRIBUTE_NAME = "secured";
+
+ /**
+ * Compare method where any of the required authorities can match the principal's authorities
+ */
+ public static final short COMPARISON_ANY = 1;
+
+ /**
+ * Compare method where all of the required authorities must match the principal's authorities
+ */
+ public static final short COMPARISON_ALL = 2;
+
+ private Collection requiredAuthorities;
+ private short comparisonType;
+
+ /**
+ * Test the required authorities against the principal's authorities
+ * @param principalAuthorities the principal's granted authorities
+ * @return true if authorized
+ */
+ public boolean isAuthorized(Collection principalAuthorities) {
+ if (getComparisonType() == COMPARISON_ANY) {
+ return isAuthorizedAny(principalAuthorities);
+ } else if (getComparisonType() == COMPARISON_ALL) {
+ return isAuthorizedAll(principalAuthorities);
+ } else {
+ throw new IllegalStateException("Unknow comparisonType");
+ }
+ }
+
+ /**
+ * Get authorities that are required but not granted
+ * @param principalAuthorities the principal's granted authorities
+ * @return non granted authorities
+ */
+ public Collection getNonGrantedAuthorities(Collection principalAuthorities) {
+ Collection nonGrantedAuthorities = new HashSet();
+ Iterator authorityIt = getRequiredAuthorities().iterator();
+ while (authorityIt.hasNext()) {
+ String authority = (String) authorityIt.next();
+ if (!principalAuthorities.contains(authority)) {
+ nonGrantedAuthorities.add(authority);
+ }
+ }
+ return nonGrantedAuthorities;
+ }
+
+ /**
+ * Convert authorities to comma separated String
+ * @param authorities the authorities to convert
+ * @return comma separated String
+ */
+ public static String convertAuthoritiesToCommaSeparatedString(Collection authorities) {
+ StringBuffer str = new StringBuffer();
+ Iterator authorityIt = authorities.iterator();
+ while (authorityIt.hasNext()) {
+ if (str.length() != 0) {
+ str.append(", ");
+ }
+ str.append(authorityIt.next());
+ }
+ return str.toString();
+ }
+
+ /**
+ * Convert authorities from comma separated String to Collection
+ * @param authorities the authorities to convert
+ * @return comma parsed Collection
+ */
+ public static Collection convertAuthoritiesFromCommaSeparatedString(String authorities) {
+ Collection auths = new HashSet();
+ Iterator authorityIt = Arrays.asList(authorities.split(",")).iterator();
+ while (authorityIt.hasNext()) {
+ String authority = ((String) authorityIt.next()).trim();
+ if (!"".equals(authority)) {
+ auths.add(authority);
+ }
+ }
+ return auths;
+ }
+
+ /**
+ * Test that any of the required authorities match the principal's authorities
+ * @param principalAuthorities the principal's granted authorities
+ * @return true if authorized
+ */
+ private boolean isAuthorizedAny(Collection principalAuthorities) {
+ boolean authorized = false;
+ Iterator authorityIt = principalAuthorities.iterator();
+ while (!authorized && authorityIt.hasNext()) {
+ if (getRequiredAuthorities().contains(authorityIt.next())) {
+ authorized = true;
+ }
+ }
+ return authorized;
+ }
+
+ /**
+ * Test that all of the required authorities match the principal's authorities
+ * @param principalAuthorities the principal's granted authorities
+ * @return true if authorized
+ */
+ private boolean isAuthorizedAll(Collection principalAuthorities) {
+ return getRequiredAuthorities().containsAll(principalAuthorities);
+ }
+
+ /**
+ * Gets required authorities
+ * @return required authorities
+ */
+ public Collection getRequiredAuthorities() {
+ return requiredAuthorities;
+ }
+
+ /**
+ * Sets required authorities
+ * @param requiredAuthorities required authorities
+ */
+ public void setRequiredAuthorities(Collection requiredAuthorities) {
+ this.requiredAuthorities = requiredAuthorities;
+ }
+
+ /**
+ * Gets comparison type
+ * @return comparison type
+ */
+ public short getComparisonType() {
+ return comparisonType;
+ }
+
+ /**
+ * Sets comparison type
+ * @param comparisonType comparison type
+ */
+ public void setComparisonType(short comparisonType) {
+ this.comparisonType = comparisonType;
+ }
+}
diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/XmlFlowBuilderTests.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/XmlFlowBuilderTests.java
index dc5a9757..02785a36 100644
--- a/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/XmlFlowBuilderTests.java
+++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/XmlFlowBuilderTests.java
@@ -8,6 +8,7 @@ import org.springframework.webflow.engine.Flow;
import org.springframework.webflow.engine.ViewState;
import org.springframework.webflow.engine.builder.FlowAssembler;
import org.springframework.webflow.engine.builder.FlowBuilderException;
+import org.springframework.webflow.security.SecurityRule;
import org.springframework.webflow.test.MockFlowBuilderContext;
public class XmlFlowBuilderTests extends TestCase {
@@ -75,6 +76,44 @@ public class XmlFlowBuilderTests extends TestCase {
assertTrue(((Boolean) flow.getAttributes().get("persistenceContext")).booleanValue());
}
+ public void testFlowSecured() {
+ ClassPathResource resource = new ClassPathResource("flow-secured.xml", getClass());
+ builder = new XmlFlowBuilder(resource);
+ FlowAssembler assembler = new FlowAssembler(builder, new MockFlowBuilderContext("flow"));
+ Flow flow = assembler.assembleFlow();
+ SecurityRule rule = (SecurityRule) flow.getAttributes().get(SecurityRule.SECURITY_AUTHORITY_ATTRIBUTE_NAME);
+ assertNotNull(rule);
+ assertEquals(SecurityRule.COMPARISON_ANY, rule.getComparisonType());
+ assertEquals(1, rule.getRequiredAuthorities().size());
+ assertTrue(rule.getRequiredAuthorities().contains("ROLE_USER"));
+ }
+
+ public void testFlowSecuredState() {
+ ClassPathResource resource = new ClassPathResource("flow-secured-state.xml", getClass());
+ builder = new XmlFlowBuilder(resource);
+ FlowAssembler assembler = new FlowAssembler(builder, new MockFlowBuilderContext("flow"));
+ Flow flow = assembler.assembleFlow();
+ SecurityRule rule = (SecurityRule) flow.getState("end").getAttributes().get(
+ SecurityRule.SECURITY_AUTHORITY_ATTRIBUTE_NAME);
+ assertNotNull(rule);
+ assertEquals(SecurityRule.COMPARISON_ANY, rule.getComparisonType());
+ assertEquals(1, rule.getRequiredAuthorities().size());
+ assertTrue(rule.getRequiredAuthorities().contains("ROLE_USER"));
+ }
+
+ public void testFlowSecuredTransition() {
+ ClassPathResource resource = new ClassPathResource("flow-secured-transition.xml", getClass());
+ builder = new XmlFlowBuilder(resource);
+ FlowAssembler assembler = new FlowAssembler(builder, new MockFlowBuilderContext("flow"));
+ Flow flow = assembler.assembleFlow();
+ SecurityRule rule = (SecurityRule) flow.getGlobalTransitionSet().toArray()[0].getAttributes().get(
+ SecurityRule.SECURITY_AUTHORITY_ATTRIBUTE_NAME);
+ assertNotNull(rule);
+ assertEquals(SecurityRule.COMPARISON_ANY, rule.getComparisonType());
+ assertEquals(1, rule.getRequiredAuthorities().size());
+ assertTrue(rule.getRequiredAuthorities().contains("ROLE_USER"));
+ }
+
public void testFlowVariable() {
ClassPathResource resource = new ClassPathResource("flow-var.xml", getClass());
builder = new XmlFlowBuilder(resource);
diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/flow-secured-state.xml b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/flow-secured-state.xml
new file mode 100644
index 00000000..1eba2dc6
--- /dev/null
+++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/flow-secured-state.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/flow-secured-transition.xml b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/flow-secured-transition.xml
new file mode 100644
index 00000000..4e56d89a
--- /dev/null
+++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/flow-secured-transition.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/flow-secured.xml b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/flow-secured.xml
new file mode 100644
index 00000000..cbcdc276
--- /dev/null
+++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/flow-secured.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/spring-webflow/src/test/java/org/springframework/webflow/security/SecurityFlowExecutionListenerTests.java b/spring-webflow/src/test/java/org/springframework/webflow/security/SecurityFlowExecutionListenerTests.java
new file mode 100644
index 00000000..1f9973ec
--- /dev/null
+++ b/spring-webflow/src/test/java/org/springframework/webflow/security/SecurityFlowExecutionListenerTests.java
@@ -0,0 +1,152 @@
+package org.springframework.webflow.security;
+
+import java.util.Collection;
+import java.util.HashSet;
+
+import junit.framework.TestCase;
+
+import org.springframework.security.AccessDeniedException;
+import org.springframework.security.Authentication;
+import org.springframework.security.GrantedAuthority;
+import org.springframework.security.GrantedAuthorityImpl;
+import org.springframework.security.context.SecurityContext;
+import org.springframework.security.context.SecurityContextHolder;
+import org.springframework.security.context.SecurityContextImpl;
+import org.springframework.security.providers.TestingAuthenticationToken;
+import org.springframework.webflow.core.collection.LocalAttributeMap;
+import org.springframework.webflow.definition.FlowDefinition;
+import org.springframework.webflow.engine.Flow;
+import org.springframework.webflow.engine.StubViewFactory;
+import org.springframework.webflow.engine.Transition;
+import org.springframework.webflow.engine.ViewState;
+import org.springframework.webflow.engine.support.DefaultTargetStateResolver;
+import org.springframework.webflow.execution.RequestContext;
+import org.springframework.webflow.test.MockRequestContext;
+
+public class SecurityFlowExecutionListenerTests extends TestCase {
+
+ public void testSessionCreatingNoSecurity() {
+ SecurityFlowExecutionListener listener = new SecurityFlowExecutionListener();
+ RequestContext context = new MockRequestContext();
+ FlowDefinition definition = new Flow("flow");
+ listener.sessionCreating(context, definition);
+ }
+
+ public void testSessionCreatingAuthorized() {
+ SecurityFlowExecutionListener listener = new SecurityFlowExecutionListener();
+ RequestContext context = new MockRequestContext();
+ Flow flow = new Flow("flow");
+ SecurityRule rule = getSecurityRuleAuthorized();
+ ((LocalAttributeMap) flow.getAttributes()).put(SecurityRule.SECURITY_AUTHORITY_ATTRIBUTE_NAME, rule);
+ configureSecurityContext();
+ listener.sessionCreating(context, flow);
+ }
+
+ public void testSessionCreatingDenied() {
+ SecurityFlowExecutionListener listener = new SecurityFlowExecutionListener();
+ RequestContext context = new MockRequestContext();
+ Flow flow = new Flow("flow");
+ SecurityRule rule = getSecurityRuleDenied();
+ ((LocalAttributeMap) flow.getAttributes()).put(SecurityRule.SECURITY_AUTHORITY_ATTRIBUTE_NAME, rule);
+ configureSecurityContext();
+ try {
+ listener.sessionCreating(context, flow);
+ fail("expected AccessDeniedException");
+ } catch (AccessDeniedException e) {
+ // success
+ }
+ }
+
+ public void testStateEnteringNoSecurity() {
+ SecurityFlowExecutionListener listener = new SecurityFlowExecutionListener();
+ RequestContext context = new MockRequestContext();
+ Flow flow = new Flow("flow");
+ ViewState state = new ViewState(flow, "view", new StubViewFactory());
+ listener.stateEntering(context, state);
+ }
+
+ public void testStateEnteringAuthorized() {
+ SecurityFlowExecutionListener listener = new SecurityFlowExecutionListener();
+ RequestContext context = new MockRequestContext();
+ Flow flow = new Flow("flow");
+ ViewState state = new ViewState(flow, "view", new StubViewFactory());
+ SecurityRule rule = getSecurityRuleAuthorized();
+ ((LocalAttributeMap) state.getAttributes()).put(SecurityRule.SECURITY_AUTHORITY_ATTRIBUTE_NAME, rule);
+ configureSecurityContext();
+ listener.stateEntering(context, state);
+ }
+
+ public void testStateEnteringDenied() {
+ SecurityFlowExecutionListener listener = new SecurityFlowExecutionListener();
+ RequestContext context = new MockRequestContext();
+ Flow flow = new Flow("flow");
+ ViewState state = new ViewState(flow, "view", new StubViewFactory());
+ SecurityRule rule = getSecurityRuleDenied();
+ ((LocalAttributeMap) state.getAttributes()).put(SecurityRule.SECURITY_AUTHORITY_ATTRIBUTE_NAME, rule);
+ configureSecurityContext();
+ try {
+ listener.stateEntering(context, state);
+ fail("expected AccessDeniedException");
+ } catch (AccessDeniedException e) {
+ // success
+ }
+ }
+
+ public void testTransitionExecutingNoSecurity() {
+ SecurityFlowExecutionListener listener = new SecurityFlowExecutionListener();
+ RequestContext context = new MockRequestContext();
+ Transition transition = new Transition(new DefaultTargetStateResolver("target"));
+ listener.transitionExecuting(context, transition);
+ }
+
+ public void testTransitionExecutingAuthorized() {
+ SecurityFlowExecutionListener listener = new SecurityFlowExecutionListener();
+ RequestContext context = new MockRequestContext();
+ Transition transition = new Transition(new DefaultTargetStateResolver("target"));
+ SecurityRule rule = getSecurityRuleAuthorized();
+ ((LocalAttributeMap) transition.getAttributes()).put(SecurityRule.SECURITY_AUTHORITY_ATTRIBUTE_NAME, rule);
+ configureSecurityContext();
+ listener.transitionExecuting(context, transition);
+ }
+
+ public void testTransitionExecutingDenied() {
+ SecurityFlowExecutionListener listener = new SecurityFlowExecutionListener();
+ RequestContext context = new MockRequestContext();
+ Transition transition = new Transition(new DefaultTargetStateResolver("target"));
+ SecurityRule rule = getSecurityRuleDenied();
+ ((LocalAttributeMap) transition.getAttributes()).put(SecurityRule.SECURITY_AUTHORITY_ATTRIBUTE_NAME, rule);
+ configureSecurityContext();
+ try {
+ listener.transitionExecuting(context, transition);
+ fail("expected AccessDeniedException");
+ } catch (AccessDeniedException e) {
+ // success
+ }
+ }
+
+ private SecurityRule getSecurityRuleAuthorized() {
+ SecurityRule rule = new SecurityRule();
+ rule.setComparisonType(SecurityRule.COMPARISON_ANY);
+ Collection authorities = new HashSet();
+ authorities.add("ROLE_USER");
+ rule.setRequiredAuthorities(authorities);
+ return rule;
+ }
+
+ private SecurityRule getSecurityRuleDenied() {
+ SecurityRule rule = new SecurityRule();
+ rule.setComparisonType(SecurityRule.COMPARISON_ANY);
+ Collection authorities = new HashSet();
+ authorities.add("ROLE_ANONYMOUS");
+ rule.setRequiredAuthorities(authorities);
+ return rule;
+ }
+
+ private void configureSecurityContext() {
+ GrantedAuthority[] authorities = { new GrantedAuthorityImpl("ROLE_USER") };
+ Authentication authentication = new TestingAuthenticationToken("test", "", authorities);
+ SecurityContext sc = new SecurityContextImpl();
+ sc.setAuthentication(authentication);
+ SecurityContextHolder.setContext(sc);
+ }
+}
diff --git a/spring-webflow/src/test/java/org/springframework/webflow/security/SecurityRuleTests.java b/spring-webflow/src/test/java/org/springframework/webflow/security/SecurityRuleTests.java
new file mode 100644
index 00000000..b1663f58
--- /dev/null
+++ b/spring-webflow/src/test/java/org/springframework/webflow/security/SecurityRuleTests.java
@@ -0,0 +1,86 @@
+package org.springframework.webflow.security;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+
+import junit.framework.Assert;
+import junit.framework.TestCase;
+
+public class SecurityRuleTests extends TestCase {
+
+ public void testAuthorizedAll() {
+ SecurityRule rule = new SecurityRule();
+ rule.setComparisonType(SecurityRule.COMPARISON_ALL);
+ Collection requiredAuthorities = new HashSet();
+ requiredAuthorities.add("ROLE_USER");
+ requiredAuthorities.add("ROLE_SUPERVISOR");
+ rule.setRequiredAuthorities(requiredAuthorities);
+ Assert.assertTrue(rule.isAuthorized(getPrincipalAuthorities()));
+ }
+
+ public void testAuthorizedAllFail() {
+ SecurityRule rule = new SecurityRule();
+ rule.setComparisonType(SecurityRule.COMPARISON_ALL);
+ Collection requiredAuthorities = new HashSet();
+ requiredAuthorities.add("ROLE_USER");
+ requiredAuthorities.add("ROLE_ANONYMOUS");
+ rule.setRequiredAuthorities(requiredAuthorities);
+ Assert.assertFalse(rule.isAuthorized(getPrincipalAuthorities()));
+ }
+
+ public void testAuthorizedAny() {
+ SecurityRule rule = new SecurityRule();
+ rule.setComparisonType(SecurityRule.COMPARISON_ANY);
+ Collection requiredAuthorities = new HashSet();
+ requiredAuthorities.add("ROLE_USER");
+ requiredAuthorities.add("ROLE_ANONYMOUS");
+ rule.setRequiredAuthorities(requiredAuthorities);
+ Assert.assertTrue(rule.isAuthorized(getPrincipalAuthorities()));
+ }
+
+ public void testAuthorizedAnyFail() {
+ SecurityRule rule = new SecurityRule();
+ rule.setComparisonType(SecurityRule.COMPARISON_ANY);
+ Collection requiredAuthorities = new HashSet();
+ requiredAuthorities.add("ROLE_NONE");
+ requiredAuthorities.add("ROLE_ANONYMOUS");
+ rule.setRequiredAuthorities(requiredAuthorities);
+ Assert.assertFalse(rule.isAuthorized(getPrincipalAuthorities()));
+ }
+
+ public void testNonGrantedAuthorities() {
+ SecurityRule rule = new SecurityRule();
+ rule.setComparisonType(SecurityRule.COMPARISON_ALL);
+ Collection requiredAuthorities = new HashSet();
+ requiredAuthorities.add("ROLE_USER");
+ requiredAuthorities.add("ROLE_ANONYMOUS");
+ rule.setRequiredAuthorities(requiredAuthorities);
+ Collection nonGrantedAuthorities = rule.getNonGrantedAuthorities(getPrincipalAuthorities());
+ Assert.assertEquals(1, nonGrantedAuthorities.size());
+ Assert.assertTrue(nonGrantedAuthorities.contains("ROLE_ANONYMOUS"));
+ }
+
+ public void testConvertAuthoritiesToCommaSeparatedString() {
+ Collection authorities = new ArrayList();
+ authorities.add("ROLE_USER");
+ authorities.add("ROLE_ANONYMOUS");
+ Assert.assertEquals("ROLE_USER, ROLE_ANONYMOUS", SecurityRule
+ .convertAuthoritiesToCommaSeparatedString(authorities));
+ }
+
+ public void testConvertAuthoritiesFromCommaSeparatedString() {
+ Collection authorities = SecurityRule
+ .convertAuthoritiesFromCommaSeparatedString(" ,,ROLE_USER, ROLE_ANONYMOUS");
+ Assert.assertEquals(2, authorities.size());
+ Assert.assertTrue(authorities.contains("ROLE_USER"));
+ Assert.assertTrue(authorities.contains("ROLE_ANONYMOUS"));
+ }
+
+ private Collection getPrincipalAuthorities() {
+ Collection principalAuthorities = new HashSet();
+ principalAuthorities.add("ROLE_USER");
+ principalAuthorities.add("ROLE_SUPERVISOR");
+ return principalAuthorities;
+ }
+}