From 859bf1eb142d9d815f6747b7b8723f0d38c83951 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Fri, 29 Feb 2008 22:57:51 +0000 Subject: [PATCH] IN PROGRESS - issue SWF-93: Add support for securing flows http://jira.springframework.org/browse/SWF-93 IN PROGRESS - issue SWF-486: Add a element for defining security attributes at flow, state, and transition level http://jira.springframework.org/browse/SWF-486 --- common-build/ivysettings.xml | 1 + spring-webflow/.classpath | 1 + spring-webflow/ivy.xml | 1 + .../engine/builder/xml/XmlFlowBuilder.java | 56 ++++++- .../engine/builder/xml/spring-webflow-2.0.xsd | 121 +++++++++++++- .../SecurityFlowExecutionListener.java | 84 ++++++++++ .../webflow/security/SecurityRule.java | 155 ++++++++++++++++++ .../builder/xml/XmlFlowBuilderTests.java | 39 +++++ .../engine/builder/xml/flow-secured-state.xml | 9 + .../builder/xml/flow-secured-transition.xml | 13 ++ .../engine/builder/xml/flow-secured.xml | 9 + .../SecurityFlowExecutionListenerTests.java | 152 +++++++++++++++++ .../webflow/security/SecurityRuleTests.java | 86 ++++++++++ 13 files changed, 717 insertions(+), 10 deletions(-) create mode 100644 spring-webflow/src/main/java/org/springframework/webflow/security/SecurityFlowExecutionListener.java create mode 100644 spring-webflow/src/main/java/org/springframework/webflow/security/SecurityRule.java create mode 100644 spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/flow-secured-state.xml create mode 100644 spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/flow-secured-transition.xml create mode 100644 spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/flow-secured.xml create mode 100644 spring-webflow/src/test/java/org/springframework/webflow/security/SecurityFlowExecutionListenerTests.java create mode 100644 spring-webflow/src/test/java/org/springframework/webflow/security/SecurityRuleTests.java 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; + } +}