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 index 830002a7..ad5c73f0 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/security/SecurityFlowExecutionListener.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/security/SecurityFlowExecutionListener.java @@ -18,17 +18,15 @@ package org.springframework.webflow.security; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.function.Function; 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.RoleVoter; -import org.springframework.security.access.vote.UnanimousBased; +import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.Assert; import org.springframework.webflow.definition.FlowDefinition; import org.springframework.webflow.definition.StateDefinition; import org.springframework.webflow.definition.TransitionDefinition; @@ -41,10 +39,26 @@ import org.springframework.webflow.execution.RequestContext; * * @author Scott Andrews */ +@SuppressWarnings("deprecation") public class SecurityFlowExecutionListener implements FlowExecutionListener { + private Function> authorizationManagerInitializer = + SecurityRule::getAuthorizationManager; + private AccessDecisionManager accessDecisionManager; + /** + * Provide a function that determines the {@link AuthorizationManager} to use + * for a given {@link SecurityRule}. + *

By default, {@link SecurityRule#getAuthorizationManager()} is used. + * @param initializer the function to use + * @since 3.0.1 + */ + public void setAuthorizationManagerInitializer(Function> initializer) { + Assert.notNull(initializer, "'initializer' is required"); + this.authorizationManagerInitializer = initializer; + } + /** * Get the access decision manager that makes flow authorization decisions. * @return the decision manager @@ -89,33 +103,45 @@ public class SecurityFlowExecutionListener implements FlowExecutionListener { * @param rule the rule to base the decision * @param object the execution listener phase */ + @SuppressWarnings("deprecation") protected void decide(SecurityRule rule, Object object) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - Collection configAttributes = getConfigAttributes(rule); - if (accessDecisionManager != null) { - accessDecisionManager.decide(authentication, object, configAttributes); + + AccessDecisionManager decisionManager = + (this.accessDecisionManager != null ? this.accessDecisionManager : createAccessDecisionManager(rule)); + + if (decisionManager != null) { + accessDecisionManager.decide(authentication, object, getConfigAttributes(rule)); } else { - createManager(rule).decide(authentication, object, configAttributes); + AuthorizationManager manager = this.authorizationManagerInitializer.apply(rule); + manager.verify(() -> authentication, object); } } - private AbstractAccessDecisionManager createManager(SecurityRule rule) { - List> voters = new ArrayList<>(); - voters.add(new RoleVoter()); - if (rule.getComparisonType() == SecurityRule.COMPARISON_ANY) { - return new AffirmativeBased(voters); - } else if (rule.getComparisonType() == SecurityRule.COMPARISON_ALL) { - return new UnanimousBased(voters); - } else { - throw new IllegalStateException("Unknown SecurityRule match type: " + rule.getComparisonType()); - } + /** + * Return an {@link AccessDecisionManager} for the SecurityRule. + *

By default, returns {@code null} in which case an + * {@link AuthorizationManager} is used instead of {@code AccessDecisionManager}. + * @param rule the rule to check + * @return the manager to use, or {@code null} + * @deprecated in favor of using an {@code AuthorizationManager} by setting + * {@link #setAuthorizationManagerInitializer(Function)} instead + */ + @SuppressWarnings("DeprecatedIsStillUsed") + @Deprecated(since = "3.0.1", forRemoval = true) + protected AccessDecisionManager createAccessDecisionManager(SecurityRule rule) { + return null; } /** * Convert SecurityRule into a form understood by Spring Security * @param rule the rule to convert * @return list of ConfigAttributes for Spring Security + * @deprecated in favor of using an {@code AuthorizationManager} by setting + * {@link #setAuthorizationManagerInitializer(Function)} instead */ + @SuppressWarnings("DeprecatedIsStillUsed") + @Deprecated(since = "3.0.1", forRemoval = true) protected Collection getConfigAttributes(SecurityRule rule) { List configAttributes = new ArrayList<>(); for (String attribute : rule.getAttributes()) { 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 index 1bb87e6d..c8bb595e 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/security/SecurityRule.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/security/SecurityRule.java @@ -1,5 +1,5 @@ /* - * Copyright 2004-2012 the original author or authors. + * Copyright 2004-2024 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. @@ -18,6 +18,9 @@ package org.springframework.webflow.security; import java.util.Collection; import java.util.HashSet; +import org.springframework.security.authorization.AuthorityAuthorizationManager; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.authorization.AuthorizationManagers; import org.springframework.util.StringUtils; /** @@ -46,6 +49,8 @@ public class SecurityRule { private short comparisonType = COMPARISON_ANY; + private AuthorizationManager authorizationManager; + /** * Convert attributes to comma separated String * @param attributes the attributes to convert @@ -102,4 +107,24 @@ public class SecurityRule { public void setComparisonType(short comparisonType) { this.comparisonType = comparisonType; } + + /** + * Return an {@link AuthorizationManager} for this security config based on + * {@link AuthorityAuthorizationManager}. + */ + @SuppressWarnings("unchecked") + public AuthorizationManager getAuthorizationManager() { + if (this.authorizationManager == null) { + this.authorizationManager = switch (this.comparisonType) { + case SecurityRule.COMPARISON_ANY -> + AuthorityAuthorizationManager.hasAnyAuthority(this.attributes.toArray(new String[0])); + case SecurityRule.COMPARISON_ALL -> AuthorizationManagers.allOf(this.attributes.stream() + .map(AuthorityAuthorizationManager::hasAuthority) + .toArray(AuthorizationManager[]::new)); + default -> throw new IllegalStateException("Unknown SecurityRule match type: " + this.comparisonType); + }; + } + return this.authorizationManager; + } + } 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 index b8553931..0780bd10 100644 --- a/spring-webflow/src/test/java/org/springframework/webflow/security/SecurityFlowExecutionListenerTests.java +++ b/spring-webflow/src/test/java/org/springframework/webflow/security/SecurityFlowExecutionListenerTests.java @@ -1,13 +1,11 @@ package org.springframework.webflow.security; -import static org.junit.jupiter.api.Assertions.fail; - -import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.List; import org.junit.jupiter.api.Test; + import org.springframework.security.access.AccessDeniedException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; @@ -25,6 +23,8 @@ import org.springframework.webflow.engine.support.DefaultTargetStateResolver; import org.springframework.webflow.execution.RequestContext; import org.springframework.webflow.test.MockRequestContext; +import static org.junit.jupiter.api.Assertions.fail; + public class SecurityFlowExecutionListenerTests { @Test @@ -167,8 +167,10 @@ public class SecurityFlowExecutionListenerTests { } private Authentication getAuthentication() { - List authorities = Arrays. asList(new SimpleGrantedAuthority("ROLE_1"), - new SimpleGrantedAuthority("ROLE_2"), new SimpleGrantedAuthority("ROLE_3")); - return new UsernamePasswordAuthenticationToken("test", "", authorities); + return new UsernamePasswordAuthenticationToken("test", "", + List.of( + new SimpleGrantedAuthority("ROLE_1"), + new SimpleGrantedAuthority("ROLE_2"), + new SimpleGrantedAuthority("ROLE_3"))); } }