1
0
mirror of synced 2026-08-05 01:36:56 +00:00

Polish AllRequiredFactorsAuthorizationManager.anyOf

- Add validation
- Extract to static inner class
- Uniqueness determined by Set rather than requiredFactor
  This is important for the failure with the same RequiredFactor, but a
  different reason
- Add documentation

Signed-off-by: Robert Winch <362503+rwinch@users.noreply.github.com>
This commit is contained in:
Robert Winch
2026-03-31 10:49:03 -05:00
parent 6b09352a93
commit ff820a868e
6 changed files with 271 additions and 21 deletions
@@ -20,11 +20,11 @@ import java.time.Clock;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@@ -54,28 +54,23 @@ public final class AllRequiredFactorsAuthorizationManager<T> implements Authoriz
/**
* 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.
* {@link AllRequiredFactorsAuthorizationManager} granted. When all managers deny,
* collects the unique {@link RequiredFactorError}s from each manager.
* @param <T> the type of object that is being authorized
* @param managers the {@link AllRequiredFactorsAuthorizationManager}s to use
* @param managers the {@link AllRequiredFactorsAuthorizationManager}s to use; cannot
* be empty or contain null elements
* @return the {@link AuthorizationManager} to use
* @since 7.1
* @see AuthorizationManagers#anyOf(AuthorizationManager[])
*/
@SafeVarargs
public static <T> AuthorizationManager<T> anyOf(AllRequiredFactorsAuthorizationManager<T>... managers) {
return (authentication, object) -> {
Map<String, RequiredFactorError> factorErrors = new LinkedHashMap<>();
for (AllRequiredFactorsAuthorizationManager<T> 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()));
};
Assert.notEmpty(managers, "managers cannot be empty");
Assert.noNullElements(managers, "managers cannot contain null elements");
if (managers.length == 1) {
return managers[0];
}
return new AnyOfFactorsAuthorizationManager<>(managers);
}
/**
@@ -179,6 +174,38 @@ public final class AllRequiredFactorsAuthorizationManager<T> implements Authoriz
return new Builder<>();
}
/**
* An {@link AuthorizationManager} that grants access if at least one
* {@link AllRequiredFactorsAuthorizationManager} granted. When all deny, collects the
* unique {@link RequiredFactorError}s from each manager.
*
* @param <T> the type of object being authorized
*/
private static final class AnyOfFactorsAuthorizationManager<T> implements AuthorizationManager<T> {
private final AllRequiredFactorsAuthorizationManager<T>[] managers;
AnyOfFactorsAuthorizationManager(AllRequiredFactorsAuthorizationManager<T>[] managers) {
Assert.notEmpty(managers, "managers cannot be empty");
Assert.noNullElements(managers, "managers cannot contain null elements");
this.managers = managers;
}
@Override
public AuthorizationResult authorize(Supplier<? extends @Nullable Authentication> authentication, T object) {
Set<RequiredFactorError> factorErrors = new LinkedHashSet<>();
for (AllRequiredFactorsAuthorizationManager<T> manager : this.managers) {
FactorAuthorizationDecision decision = manager.authorize(authentication, object);
if (decision.isGranted()) {
return decision;
}
factorErrors.addAll(decision.getFactorErrors());
}
return new FactorAuthorizationDecision(List.copyOf(factorErrors));
}
}
/**
* A builder for {@link AllRequiredFactorsAuthorizationManager}.
*
@@ -44,11 +44,11 @@ class AllRequiredFactorsAuthorizationManagerTests {
private static final Object DOES_NOT_MATTER = new Object();
private static final RequiredFactor REQUIRED_PASSWORD = RequiredFactor
private static RequiredFactor REQUIRED_PASSWORD = RequiredFactor
.withAuthority(FactorGrantedAuthority.PASSWORD_AUTHORITY)
.build();
private static final RequiredFactor EXPIRING_PASSWORD = RequiredFactor
private static RequiredFactor EXPIRING_PASSWORD = RequiredFactor
.withAuthority(FactorGrantedAuthority.PASSWORD_AUTHORITY)
.validDuration(Duration.ofHours(1))
.build();
@@ -255,7 +255,7 @@ class AllRequiredFactorsAuthorizationManagerTests {
}
@Test
void anyOfWhenRequiredFactorMissingThenMissing() {
void anyOfWhenSameAuthorityDifferentValidDurationThenBothErrorsReturned() {
AllRequiredFactorsAuthorizationManager<Object> passwordAndOtt = AllRequiredFactorsAuthorizationManager.builder()
.requireFactor(REQUIRED_PASSWORD)
.requireFactor(REQUIRED_OTT)
@@ -273,10 +273,62 @@ class AllRequiredFactorsAuthorizationManagerTests {
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));
assertThat(decision.getFactorErrors()).containsExactly(RequiredFactorError.createMissing(REQUIRED_OTT),
RequiredFactorError.createMissing(EXPIRING_OTT));
});
}
@Test
void anyOfWhenIdenticalErrorInMultipleManagersThenDeduplicated() {
AllRequiredFactorsAuthorizationManager<Object> passwordAndOtt = AllRequiredFactorsAuthorizationManager.builder()
.requireFactor(REQUIRED_PASSWORD)
.requireFactor(REQUIRED_OTT)
.build();
AllRequiredFactorsAuthorizationManager<Object> passwordOnly = AllRequiredFactorsAuthorizationManager.builder()
.requireFactor(REQUIRED_PASSWORD)
.build();
AuthorizationManager<Object> anyOf = AllRequiredFactorsAuthorizationManager.anyOf(passwordAndOtt, passwordOnly);
Authentication authentication = new TestingAuthenticationToken("user", "password", "ROLE_USER");
AuthorizationResult result = anyOf.authorize(() -> authentication, DOES_NOT_MATTER);
assertThat(result).asInstanceOf(type(FactorAuthorizationDecision.class)).satisfies((decision) -> {
assertThat(decision.isGranted()).isFalse();
assertThat(decision.getFactorErrors()).containsOnly(RequiredFactorError.createMissing(REQUIRED_PASSWORD),
RequiredFactorError.createMissing(REQUIRED_OTT));
});
}
@Test
void anyOfWhenDeniedThenErrorsRetainedInManagerOrder() {
AllRequiredFactorsAuthorizationManager<Object> passwordOnly = AllRequiredFactorsAuthorizationManager.builder()
.requireFactor(REQUIRED_PASSWORD)
.build();
AllRequiredFactorsAuthorizationManager<Object> ottOnly = AllRequiredFactorsAuthorizationManager.builder()
.requireFactor(REQUIRED_OTT)
.build();
AuthorizationManager<Object> anyOf = AllRequiredFactorsAuthorizationManager.anyOf(passwordOnly, ottOnly);
Authentication authentication = new TestingAuthenticationToken("user", "password", "ROLE_USER");
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_PASSWORD),
RequiredFactorError.createMissing(REQUIRED_OTT));
});
}
@Test
void anyOfWhenEmptyManagersThenIllegalArgumentException() {
assertThatIllegalArgumentException().isThrownBy(() -> AllRequiredFactorsAuthorizationManager.anyOf());
}
@Test
void anyOfWhenSingleManagerThenReturnsSameInstance() {
AllRequiredFactorsAuthorizationManager<Object> manager = AllRequiredFactorsAuthorizationManager.builder()
.requireFactor(REQUIRED_PASSWORD)
.build();
AuthorizationManager<Object> result = AllRequiredFactorsAuthorizationManager.anyOf(manager);
assertThat(result == manager).isTrue();
}
@Test
void setClockWhenNullThenIllegalArgumentException() {
AllRequiredFactorsAuthorizationManager<Object> allFactors = AllRequiredFactorsAuthorizationManager.builder()