Skip to content

Commit 1499cf3

Browse files
committed
Add AllRequiredFactorsAuthorizationManager.anyOf
Closes gh-18960 Signed-off-by: Evgeniy Cheban <mister.cheban@gmail.com>
1 parent aff7369 commit 1499cf3

File tree

2 files changed

+89
-2
lines changed

2 files changed

+89
-2
lines changed

core/src/main/java/org/springframework/security/authorization/AllRequiredFactorsAuthorizationManager.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
import java.time.Instant;
2121
import java.util.ArrayList;
2222
import java.util.Collections;
23+
import java.util.LinkedHashMap;
2324
import java.util.List;
25+
import java.util.Map;
2426
import java.util.Objects;
2527
import java.util.Optional;
2628
import java.util.function.Consumer;
@@ -40,6 +42,7 @@
4042
* is not expired for each {@link RequiredFactor}.
4143
*
4244
* @author Rob Winch
45+
* @author Evgeniy Cheban
4346
* @since 7.0
4447
* @see AuthorityAuthorizationManager
4548
*/
@@ -49,6 +52,32 @@ public final class AllRequiredFactorsAuthorizationManager<T> implements Authoriz
4952

5053
private final List<RequiredFactor> requiredFactors;
5154

55+
/**
56+
* Creates an {@link AuthorizationManager} that grants access if at least one
57+
* {@link AllRequiredFactorsAuthorizationManager} granted, collects
58+
* {@link RequiredFactorError}s omitting duplicate errors of the same factor.
59+
* @param <T> the type of object that is being authorized
60+
* @param managers the {@link AllRequiredFactorsAuthorizationManager}s to use
61+
* @return the {@link AuthorizationManager} to use
62+
* @since 7.1
63+
* @see AuthorizationManagers#anyOf(AuthorizationManager[])
64+
*/
65+
@SafeVarargs
66+
public static <T> AuthorizationManager<T> anyOf(AllRequiredFactorsAuthorizationManager<T>... managers) {
67+
return (authentication, object) -> {
68+
Map<String, RequiredFactorError> factorErrors = new LinkedHashMap<>();
69+
for (AllRequiredFactorsAuthorizationManager<T> manager : managers) {
70+
FactorAuthorizationDecision decision = manager.authorize(authentication, object);
71+
if (decision.isGranted()) {
72+
return decision;
73+
}
74+
decision.getFactorErrors()
75+
.forEach((e) -> factorErrors.putIfAbsent(e.getRequiredFactor().getAuthority(), e));
76+
}
77+
return new FactorAuthorizationDecision(List.copyOf(factorErrors.values()));
78+
};
79+
}
80+
5281
/**
5382
* Creates a new instance.
5483
* @param requiredFactors the authorities that are required.

core/src/test/java/org/springframework/security/authorization/AllRequiredFactorsAuthorizationManagerTests.java

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,26 +31,37 @@
3131
import static org.assertj.core.api.Assertions.assertThat;
3232
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
3333
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
34+
import static org.assertj.core.api.InstanceOfAssertFactories.type;
3435

3536
/**
3637
* Test {@link AllRequiredFactorsAuthorizationManager}.
3738
*
3839
* @author Rob Winch
40+
* @author Evgeniy Cheban
3941
* @since 7.0
4042
*/
4143
class AllRequiredFactorsAuthorizationManagerTests {
4244

4345
private static final Object DOES_NOT_MATTER = new Object();
4446

45-
private static RequiredFactor REQUIRED_PASSWORD = RequiredFactor
47+
private static final RequiredFactor REQUIRED_PASSWORD = RequiredFactor
4648
.withAuthority(FactorGrantedAuthority.PASSWORD_AUTHORITY)
4749
.build();
4850

49-
private static RequiredFactor EXPIRING_PASSWORD = RequiredFactor
51+
private static final RequiredFactor EXPIRING_PASSWORD = RequiredFactor
5052
.withAuthority(FactorGrantedAuthority.PASSWORD_AUTHORITY)
5153
.validDuration(Duration.ofHours(1))
5254
.build();
5355

56+
private static final RequiredFactor REQUIRED_OTT = RequiredFactor
57+
.withAuthority(FactorGrantedAuthority.OTT_AUTHORITY)
58+
.build();
59+
60+
private static final RequiredFactor EXPIRING_OTT = RequiredFactor
61+
.withAuthority(FactorGrantedAuthority.OTT_AUTHORITY)
62+
.validDuration(Duration.ofHours(1))
63+
.build();
64+
5465
@Test
5566
void authorizeWhenGranted() {
5667
AllRequiredFactorsAuthorizationManager<Object> allFactors = AllRequiredFactorsAuthorizationManager.builder()
@@ -219,6 +230,53 @@ void authorizeWhenDifferentFactorGrantedAuthorityThenMissing() {
219230
assertThat(result.getFactorErrors()).containsExactly(RequiredFactorError.createMissing(REQUIRED_PASSWORD));
220231
}
221232

233+
@Test
234+
void anyOfWhenOneGrantedThenGranted() {
235+
AllRequiredFactorsAuthorizationManager<Object> expiringPasswordAndOtt = AllRequiredFactorsAuthorizationManager
236+
.builder()
237+
.requireFactor(EXPIRING_PASSWORD)
238+
.requireFactor(EXPIRING_OTT)
239+
.build();
240+
AllRequiredFactorsAuthorizationManager<Object> passwordAndExpiringOtt = AllRequiredFactorsAuthorizationManager
241+
.builder()
242+
.requireFactor(REQUIRED_PASSWORD)
243+
.requireFactor(EXPIRING_OTT)
244+
.build();
245+
FactorGrantedAuthority passwordFactor = FactorGrantedAuthority.withAuthority(EXPIRING_PASSWORD.getAuthority())
246+
.issuedAt(Instant.now().minus(Duration.ofHours(2)))
247+
.build();
248+
FactorGrantedAuthority ottFactor = FactorGrantedAuthority.withAuthority(EXPIRING_OTT.getAuthority()).build();
249+
AuthorizationManager<Object> anyOf = AllRequiredFactorsAuthorizationManager.anyOf(expiringPasswordAndOtt,
250+
passwordAndExpiringOtt);
251+
Authentication authentication = new TestingAuthenticationToken("user", "password", passwordFactor, ottFactor);
252+
AuthorizationResult result = anyOf.authorize(() -> authentication, DOES_NOT_MATTER);
253+
assertThat(result).isNotNull();
254+
assertThat(result.isGranted()).isTrue();
255+
}
256+
257+
@Test
258+
void anyOfWhenRequiredFactorMissingThenMissing() {
259+
AllRequiredFactorsAuthorizationManager<Object> passwordAndOtt = AllRequiredFactorsAuthorizationManager.builder()
260+
.requireFactor(REQUIRED_PASSWORD)
261+
.requireFactor(REQUIRED_OTT)
262+
.build();
263+
AllRequiredFactorsAuthorizationManager<Object> passwordAndExpiringOtt = AllRequiredFactorsAuthorizationManager
264+
.builder()
265+
.requireFactor(REQUIRED_PASSWORD)
266+
.requireFactor(EXPIRING_OTT)
267+
.build();
268+
FactorGrantedAuthority passwordFactor = FactorGrantedAuthority.withAuthority(REQUIRED_PASSWORD.getAuthority())
269+
.build();
270+
AuthorizationManager<Object> anyOf = AllRequiredFactorsAuthorizationManager.anyOf(passwordAndOtt,
271+
passwordAndExpiringOtt);
272+
Authentication authentication = new TestingAuthenticationToken("user", "password", passwordFactor);
273+
AuthorizationResult result = anyOf.authorize(() -> authentication, DOES_NOT_MATTER);
274+
assertThat(result).asInstanceOf(type(FactorAuthorizationDecision.class)).satisfies((decision) -> {
275+
assertThat(decision.isGranted()).isFalse();
276+
assertThat(decision.getFactorErrors()).containsExactly(RequiredFactorError.createMissing(REQUIRED_OTT));
277+
});
278+
}
279+
222280
@Test
223281
void setClockWhenNullThenIllegalArgumentException() {
224282
AllRequiredFactorsAuthorizationManager<Object> allFactors = AllRequiredFactorsAuthorizationManager.builder()

0 commit comments

Comments
 (0)