diff --git a/core/src/main/java/org/springframework/security/authorization/AllRequiredFactorsAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/AllRequiredFactorsAuthorizationManager.java index 8691b0333c..e1ba0fc726 100644 --- a/core/src/main/java/org/springframework/security/authorization/AllRequiredFactorsAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/AllRequiredFactorsAuthorizationManager.java @@ -20,7 +20,9 @@ import java.time.Clock; import java.time.Instant; import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; @@ -40,6 +42,7 @@ import org.springframework.util.Assert; * is not expired for each {@link RequiredFactor}. * * @author Rob Winch + * @author Evgeniy Cheban * @since 7.0 * @see AuthorityAuthorizationManager */ @@ -49,6 +52,32 @@ public final class AllRequiredFactorsAuthorizationManager implements Authoriz private final List requiredFactors; + /** + * Creates an {@link AuthorizationManager} that grants access if at least one + * {@link AllRequiredFactorsAuthorizationManager} granted, collects + * {@link RequiredFactorError}s omitting duplicate errors of the same factor. + * @param the type of object that is being authorized + * @param managers the {@link AllRequiredFactorsAuthorizationManager}s to use + * @return the {@link AuthorizationManager} to use + * @since 7.1 + * @see AuthorizationManagers#anyOf(AuthorizationManager[]) + */ + @SafeVarargs + public static AuthorizationManager anyOf(AllRequiredFactorsAuthorizationManager... managers) { + return (authentication, object) -> { + Map factorErrors = new LinkedHashMap<>(); + for (AllRequiredFactorsAuthorizationManager manager : managers) { + FactorAuthorizationDecision decision = manager.authorize(authentication, object); + if (decision.isGranted()) { + return decision; + } + decision.getFactorErrors() + .forEach((e) -> factorErrors.putIfAbsent(e.getRequiredFactor().getAuthority(), e)); + } + return new FactorAuthorizationDecision(List.copyOf(factorErrors.values())); + }; + } + /** * Creates a new instance. * @param requiredFactors the authorities that are required. diff --git a/core/src/test/java/org/springframework/security/authorization/AllRequiredFactorsAuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/AllRequiredFactorsAuthorizationManagerTests.java index 8d544727d9..ce9d10e2b0 100644 --- a/core/src/test/java/org/springframework/security/authorization/AllRequiredFactorsAuthorizationManagerTests.java +++ b/core/src/test/java/org/springframework/security/authorization/AllRequiredFactorsAuthorizationManagerTests.java @@ -31,26 +31,37 @@ import org.springframework.security.core.authority.FactorGrantedAuthority; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.InstanceOfAssertFactories.type; /** * Test {@link AllRequiredFactorsAuthorizationManager}. * * @author Rob Winch + * @author Evgeniy Cheban * @since 7.0 */ class AllRequiredFactorsAuthorizationManagerTests { private static final Object DOES_NOT_MATTER = new Object(); - private static RequiredFactor REQUIRED_PASSWORD = RequiredFactor + private static final RequiredFactor REQUIRED_PASSWORD = RequiredFactor .withAuthority(FactorGrantedAuthority.PASSWORD_AUTHORITY) .build(); - private static RequiredFactor EXPIRING_PASSWORD = RequiredFactor + private static final RequiredFactor EXPIRING_PASSWORD = RequiredFactor .withAuthority(FactorGrantedAuthority.PASSWORD_AUTHORITY) .validDuration(Duration.ofHours(1)) .build(); + private static final RequiredFactor REQUIRED_OTT = RequiredFactor + .withAuthority(FactorGrantedAuthority.OTT_AUTHORITY) + .build(); + + private static final RequiredFactor EXPIRING_OTT = RequiredFactor + .withAuthority(FactorGrantedAuthority.OTT_AUTHORITY) + .validDuration(Duration.ofHours(1)) + .build(); + @Test void authorizeWhenGranted() { AllRequiredFactorsAuthorizationManager allFactors = AllRequiredFactorsAuthorizationManager.builder() @@ -219,6 +230,53 @@ class AllRequiredFactorsAuthorizationManagerTests { assertThat(result.getFactorErrors()).containsExactly(RequiredFactorError.createMissing(REQUIRED_PASSWORD)); } + @Test + void anyOfWhenOneGrantedThenGranted() { + AllRequiredFactorsAuthorizationManager expiringPasswordAndOtt = AllRequiredFactorsAuthorizationManager + .builder() + .requireFactor(EXPIRING_PASSWORD) + .requireFactor(EXPIRING_OTT) + .build(); + AllRequiredFactorsAuthorizationManager passwordAndExpiringOtt = AllRequiredFactorsAuthorizationManager + .builder() + .requireFactor(REQUIRED_PASSWORD) + .requireFactor(EXPIRING_OTT) + .build(); + FactorGrantedAuthority passwordFactor = FactorGrantedAuthority.withAuthority(EXPIRING_PASSWORD.getAuthority()) + .issuedAt(Instant.now().minus(Duration.ofHours(2))) + .build(); + FactorGrantedAuthority ottFactor = FactorGrantedAuthority.withAuthority(EXPIRING_OTT.getAuthority()).build(); + AuthorizationManager anyOf = AllRequiredFactorsAuthorizationManager.anyOf(expiringPasswordAndOtt, + passwordAndExpiringOtt); + Authentication authentication = new TestingAuthenticationToken("user", "password", passwordFactor, ottFactor); + AuthorizationResult result = anyOf.authorize(() -> authentication, DOES_NOT_MATTER); + assertThat(result).isNotNull(); + assertThat(result.isGranted()).isTrue(); + } + + @Test + void anyOfWhenRequiredFactorMissingThenMissing() { + AllRequiredFactorsAuthorizationManager passwordAndOtt = AllRequiredFactorsAuthorizationManager.builder() + .requireFactor(REQUIRED_PASSWORD) + .requireFactor(REQUIRED_OTT) + .build(); + AllRequiredFactorsAuthorizationManager passwordAndExpiringOtt = AllRequiredFactorsAuthorizationManager + .builder() + .requireFactor(REQUIRED_PASSWORD) + .requireFactor(EXPIRING_OTT) + .build(); + FactorGrantedAuthority passwordFactor = FactorGrantedAuthority.withAuthority(REQUIRED_PASSWORD.getAuthority()) + .build(); + AuthorizationManager anyOf = AllRequiredFactorsAuthorizationManager.anyOf(passwordAndOtt, + passwordAndExpiringOtt); + Authentication authentication = new TestingAuthenticationToken("user", "password", passwordFactor); + AuthorizationResult result = anyOf.authorize(() -> authentication, DOES_NOT_MATTER); + assertThat(result).asInstanceOf(type(FactorAuthorizationDecision.class)).satisfies((decision) -> { + assertThat(decision.isGranted()).isFalse(); + assertThat(decision.getFactorErrors()).containsExactly(RequiredFactorError.createMissing(REQUIRED_OTT)); + }); + } + @Test void setClockWhenNullThenIllegalArgumentException() { AllRequiredFactorsAuthorizationManager allFactors = AllRequiredFactorsAuthorizationManager.builder()