Add AllFactorsAuthorizationManager

Closes gh-17997
This commit is contained in:
Rob Winch 2025-10-02 11:32:53 -05:00
parent 3f74991ce9
commit d1ff983c11
No known key found for this signature in database
8 changed files with 968 additions and 0 deletions

View File

@ -0,0 +1,196 @@
/*
* Copyright 2004-present 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.authorization;
import java.time.Clock;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.jspecify.annotations.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.FactorGrantedAuthority;
import org.springframework.util.Assert;
/**
* An {@link AuthorizationManager} that determines if the current user is authorized by
* evaluating if the {@link Authentication} contains a {@link FactorGrantedAuthority} that
* is not expired for each {@link RequiredFactor}.
*
* @author Rob Winch
* @since 7.0
* @see AuthorityAuthorizationManager
*/
public final class AllFactorsAuthorizationManager<T> implements AuthorizationManager<T> {
private Clock clock = Clock.systemUTC();
private final List<RequiredFactor> requiredFactors;
/**
* Creates a new instance.
* @param requiredFactors the authorities that are required.
*/
private AllFactorsAuthorizationManager(List<RequiredFactor> requiredFactors) {
Assert.notEmpty(requiredFactors, "requiredFactors cannot be empty");
Assert.noNullElements(requiredFactors, "requiredFactors must not contain null elements");
this.requiredFactors = Collections.unmodifiableList(requiredFactors);
}
/**
* Sets the {@link Clock} to use.
* @param clock the {@link Clock} to use. Cannot be null.
*/
public void setClock(Clock clock) {
Assert.notNull(clock, "clock cannot be null");
this.clock = clock;
}
/**
* For each {@link RequiredFactor} finds the first
* {@link FactorGrantedAuthority#getAuthority()} that matches the
* {@link RequiredFactor#getAuthority()}. The
* {@link FactorGrantedAuthority#getIssuedAt()} must be more recent than
* {@link RequiredFactor#getValidDuration()} (if non-null).
* @param authentication the {@link Supplier} of the {@link Authentication} to check
* @param object the object to check authorization on (not used).
* @return an {@link FactorAuthorizationDecision}
*/
@Override
public FactorAuthorizationDecision authorize(Supplier<? extends @Nullable Authentication> authentication,
T object) {
List<FactorGrantedAuthority> currentFactorAuthorities = getFactorGrantedAuthorities(authentication.get());
List<RequiredFactorError> factorErrors = this.requiredFactors.stream()
.map((factor) -> requiredFactorError(factor, currentFactorAuthorities))
.filter(Objects::nonNull)
.toList();
return new FactorAuthorizationDecision(factorErrors);
}
/**
* Given the {@link RequiredFactor} and the current {@link FactorGrantedAuthority}
* instances, returns {@link RequiredFactor} or null if granted.
* @param requiredFactor the {@link RequiredFactor} to check.
* @param currentFactors the current user's {@link FactorGrantedAuthority}.
* @return the {@link RequiredFactor} or null if granted.
*/
private @Nullable RequiredFactorError requiredFactorError(RequiredFactor requiredFactor,
List<FactorGrantedAuthority> currentFactors) {
Optional<FactorGrantedAuthority> matchingAuthority = currentFactors.stream()
.filter((authority) -> authority.getAuthority().equals(requiredFactor.getAuthority()))
.findFirst();
if (!matchingAuthority.isPresent()) {
return RequiredFactorError.createMissing(requiredFactor);
}
return matchingAuthority.map((authority) -> {
if (requiredFactor.getValidDuration() == null) {
// granted (only requires authority to match)
return null;
}
Instant now = this.clock.instant();
Instant expiresAt = authority.getIssuedAt().plus(requiredFactor.getValidDuration());
if (now.isBefore(expiresAt)) {
// granted
return null;
}
// denied (expired)
return RequiredFactorError.createExpired(requiredFactor);
}).orElse(null);
}
/**
* Extracts all of the {@link FactorGrantedAuthority} instances from
* {@link Authentication#getAuthorities()}. If {@link Authentication} is null, or
* {@link Authentication#isAuthenticated()} is false, then an empty {@link List} is
* returned.
* @param authentication the {@link Authentication} (possibly null).
* @return all of the {@link FactorGrantedAuthority} instances from
* {@link Authentication#getAuthorities()}.
*/
private List<FactorGrantedAuthority> getFactorGrantedAuthorities(@Nullable Authentication authentication) {
if (authentication == null || !authentication.isAuthenticated()) {
return Collections.emptyList();
}
// @formatter:off
return authentication.getAuthorities().stream()
.filter(FactorGrantedAuthority.class::isInstance)
.map(FactorGrantedAuthority.class::cast)
.collect(Collectors.toList());
// @formatter:on
}
/**
* Creates a new {@link Builder}
* @return
*/
public static Builder builder() {
return new Builder();
}
/**
* A builder for {@link AllFactorsAuthorizationManager}.
*
* @author Rob Winch
* @since 7.0
*/
public static final class Builder {
private List<RequiredFactor> requiredFactors = new ArrayList<>();
/**
* Allows the user to consume the {@link RequiredFactor.Builder} that is passed in
* and then adds the result to the {@link #requiredFactor(RequiredFactor)}.
* @param requiredFactor the {@link Consumer} to invoke.
* @return the builder.
*/
public Builder requiredFactor(Consumer<RequiredFactor.Builder> requiredFactor) {
Assert.notNull(requiredFactor, "requiredFactor cannot be null");
RequiredFactor.Builder builder = RequiredFactor.builder();
requiredFactor.accept(builder);
return requiredFactor(builder.build());
}
/**
* The {@link RequiredFactor} to add.
* @param requiredFactor the requiredFactor to add. Cannot be null.
* @return the builder.
*/
public Builder requiredFactor(RequiredFactor requiredFactor) {
Assert.notNull(requiredFactor, "requiredFactor cannot be null");
this.requiredFactors.add(requiredFactor);
return this;
}
/**
* Builds the {@link AllFactorsAuthorizationManager}.
* @param <T> the type.
* @return the {@link AllFactorsAuthorizationManager}
*/
public <T> AllFactorsAuthorizationManager<T> build() {
return new AllFactorsAuthorizationManager<T>(this.requiredFactors);
}
}
}

View File

@ -0,0 +1,62 @@
/*
* Copyright 2004-present 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.authorization;
import java.util.Collections;
import java.util.List;
import org.springframework.util.Assert;
/**
* An {@link AuthorizationResult} that contains {@link RequiredFactorError}.
*
* @author Rob Winch
* @since 7.0
*/
public class FactorAuthorizationDecision implements AuthorizationResult {
private final List<RequiredFactorError> factorErrors;
/**
* Creates a new instance.
* @param factorErrors the {@link RequiredFactorError}. If empty, {@link #isGranted()}
* returns true. Cannot be null or contain empty values.
*/
public FactorAuthorizationDecision(List<RequiredFactorError> factorErrors) {
Assert.notNull(factorErrors, "factorErrors cannot be null");
Assert.noNullElements(factorErrors, "factorErrors must not contain null elements");
this.factorErrors = Collections.unmodifiableList(factorErrors);
}
/**
* The specified {@link RequiredFactorError}s
* @return the errors. Cannot be null or contain null values.
*/
public List<RequiredFactorError> getFactorErrors() {
return this.factorErrors;
}
/**
* Returns {@code getFactorErrors().isEmpty()}.
* @return
*/
@Override
public boolean isGranted() {
return this.factorErrors.isEmpty();
}
}

View File

@ -0,0 +1,142 @@
/*
* Copyright 2004-present 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.authorization;
import java.time.Duration;
import java.util.Objects;
import org.jspecify.annotations.Nullable;
import org.springframework.security.core.authority.FactorGrantedAuthority;
import org.springframework.util.Assert;
/**
* The requirements for an {@link FactorGrantedAuthority} to be considered valid.
*
* @author Rob Winch
* @since 7.0
*/
public final class RequiredFactor {
private final String authority;
private final @Nullable Duration validDuration;
private RequiredFactor(String authority, @Nullable Duration validDuration) {
Assert.notNull(authority, "authority cannot be null");
this.authority = authority;
this.validDuration = validDuration;
}
/**
* The {@link FactorGrantedAuthority#getAuthority()}.
* @return the authority.
*/
public String getAuthority() {
return this.authority;
}
/**
* How long the
* {@link org.springframework.security.core.authority.FactorGrantedAuthority} is valid
* for.
* @return
*/
public @Nullable Duration getValidDuration() {
return this.validDuration;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof RequiredFactor that)) {
return false;
}
return Objects.equals(this.authority, that.authority) && Objects.equals(this.validDuration, that.validDuration);
}
@Override
public int hashCode() {
return Objects.hash(this.authority, this.validDuration);
}
@Override
public String toString() {
return "RequiredFactor [authority=" + this.authority + ", validDuration=" + this.validDuration + "]";
}
/**
* Creates a {@link Builder} with the specified authority.
* @param authority the authority.
* @return the builder.
*/
public static Builder withAuthority(String authority) {
return builder().authority(authority);
}
/**
* Creates a new {@link Builder}.
* @return
*/
public static Builder builder() {
return new Builder();
}
/**
* A builder for {@link RequiredFactor}.
*
* @author Rob Winch
* @since 7.0
*/
public static class Builder {
private @Nullable String authority;
private @Nullable Duration validDuration;
/**
* Sets the required authority.
* @param authority the authority.
* @return the builder.
*/
public Builder authority(String authority) {
this.authority = authority;
return this;
}
/**
* Sets the optional {@link Duration} of time that the {@link RequiredFactor} is
* valid for.
* @param validDuration the {@link Duration}.
* @return
*/
public Builder validDuration(Duration validDuration) {
this.validDuration = validDuration;
return this;
}
/**
* Builds a new instance.
* @return
*/
public RequiredFactor build() {
Assert.notNull(this.authority, "authority cannot be null");
return new RequiredFactor(this.authority, this.validDuration);
}
}
}

View File

@ -0,0 +1,118 @@
/*
* Copyright 2004-present 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.authorization;
import java.util.Objects;
import org.springframework.security.core.authority.FactorGrantedAuthority;
import org.springframework.util.Assert;
/**
* An error when the requirements of {@link RequiredFactor} are not met.
*
* @author Rob Winch
* @since 7.0
*/
public class RequiredFactorError {
private final RequiredFactor requiredFactor;
private final Reason reason;
RequiredFactorError(RequiredFactor requiredFactor, Reason reason) {
Assert.notNull(requiredFactor, "RequiredFactor must not be null");
Assert.notNull(reason, "Reason must not be null");
if (reason == Reason.EXPIRED && requiredFactor.getValidDuration() == null) {
throw new IllegalArgumentException(
"If expired, RequiredFactor.getValidDuration() must not be null. Got " + requiredFactor);
}
this.requiredFactor = requiredFactor;
this.reason = reason;
}
public RequiredFactor getRequiredFactor() {
return this.requiredFactor;
}
/**
* True if not {@link #isMissing()} but was older than the
* {@link RequiredFactor#getValidDuration()}.
* @return true if expired, else false
*/
public boolean isExpired() {
return this.reason == Reason.EXPIRED;
}
/**
* True if no {@link FactorGrantedAuthority#getAuthority()} on the
* {@link org.springframework.security.core.Authentication} matched
* {@link RequiredFactor#getAuthority()}.
* @return true if missing, else false.
*/
public boolean isMissing() {
return this.reason == Reason.MISSING;
}
@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) {
return false;
}
RequiredFactorError that = (RequiredFactorError) o;
return Objects.equals(this.requiredFactor, that.requiredFactor) && this.reason == that.reason;
}
@Override
public int hashCode() {
return Objects.hash(this.requiredFactor, this.reason);
}
@Override
public String toString() {
return "RequiredFactorError{" + "requiredFactor=" + this.requiredFactor + ", reason=" + this.reason + '}';
}
public static RequiredFactorError createMissing(RequiredFactor requiredFactor) {
return new RequiredFactorError(requiredFactor, Reason.MISSING);
}
public static RequiredFactorError createExpired(RequiredFactor requiredFactor) {
return new RequiredFactorError(requiredFactor, Reason.EXPIRED);
}
/**
* The reason that the error occurred.
*
* @author Rob Winch
* @since 7.0
*/
private enum Reason {
/**
* The authority was missing.
* @see #isMissing()
*/
MISSING,
/**
* The authority was considered expired.
* @see #isExpired()
*/
EXPIRED
}
}

View File

@ -0,0 +1,249 @@
/*
* Copyright 2004-present 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.authorization;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.util.function.Consumer;
import org.junit.jupiter.api.Test;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthorities;
import org.springframework.security.core.authority.FactorGrantedAuthority;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Test {@link AllFactorsAuthorizationManager}.
*
* @author Rob Winch
* @since 7.0
*/
class AllFactorsAuthorizationManagerTests {
private static final Object DOES_NOT_MATTER = new Object();
private static RequiredFactor REQUIRED_PASSWORD = RequiredFactor
.withAuthority(GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY)
.build();
private static RequiredFactor EXPIRING_PASSWORD = RequiredFactor
.withAuthority(GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY)
.validDuration(Duration.ofHours(1))
.build();
@Test
void authorizeWhenGranted() {
AllFactorsAuthorizationManager<Object> allFactors = AllFactorsAuthorizationManager.builder()
.requiredFactor(REQUIRED_PASSWORD)
.build();
FactorGrantedAuthority passwordFactor = FactorGrantedAuthority.withAuthority(REQUIRED_PASSWORD.getAuthority())
.issuedAt(Instant.now())
.build();
Authentication authentication = new TestingAuthenticationToken("user", "password", passwordFactor);
FactorAuthorizationDecision result = allFactors.authorize(() -> authentication, DOES_NOT_MATTER);
assertThat(result.isGranted()).isTrue();
}
@Test
void authorizeWhenConsumerGranted() {
AllFactorsAuthorizationManager<Object> allFactors = AllFactorsAuthorizationManager.builder()
.requiredFactor((required) -> required.authority(GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY))
.build();
FactorGrantedAuthority passwordFactor = FactorGrantedAuthority
.withAuthority(GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY)
.issuedAt(Instant.now())
.build();
Authentication authentication = new TestingAuthenticationToken("user", "password", passwordFactor);
FactorAuthorizationDecision result = allFactors.authorize(() -> authentication, DOES_NOT_MATTER);
assertThat(result.isGranted()).isTrue();
}
@Test
void authorizeWhenUnauthenticated() {
AllFactorsAuthorizationManager<Object> allFactors = AllFactorsAuthorizationManager.builder()
.requiredFactor(REQUIRED_PASSWORD)
.build();
FactorGrantedAuthority passwordFactor = FactorGrantedAuthority.withAuthority(REQUIRED_PASSWORD.getAuthority())
.issuedAt(Instant.now())
.build();
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password", passwordFactor);
authentication.setAuthenticated(false);
FactorAuthorizationDecision result = allFactors.authorize(() -> authentication, DOES_NOT_MATTER);
assertThat(result.isGranted()).isFalse();
assertThat(result.getFactorErrors()).containsExactly(RequiredFactorError.createMissing(REQUIRED_PASSWORD));
}
@Test
void authorizeWhenNullAuthentication() {
AllFactorsAuthorizationManager<Object> allFactors = AllFactorsAuthorizationManager.builder()
.requiredFactor(EXPIRING_PASSWORD)
.build();
Authentication authentication = null;
FactorAuthorizationDecision result = allFactors.authorize(() -> authentication, DOES_NOT_MATTER);
assertThat(result.isGranted()).isFalse();
assertThat(result.getFactorErrors()).containsExactly(RequiredFactorError.createMissing(EXPIRING_PASSWORD));
}
@Test
void authorizeWhenRequiredFactorHasNullDurationThenNullIssuedAtGranted() {
AllFactorsAuthorizationManager<Object> allFactors = AllFactorsAuthorizationManager.builder()
.requiredFactor(REQUIRED_PASSWORD)
.build();
FactorGrantedAuthority passwordFactor = FactorGrantedAuthority.withAuthority(REQUIRED_PASSWORD.getAuthority())
.build();
Authentication authentication = new TestingAuthenticationToken("user", "password", passwordFactor);
FactorAuthorizationDecision result = allFactors.authorize(() -> authentication, DOES_NOT_MATTER);
assertThat(result.isGranted()).isTrue();
}
@Test
void authorizeWhenRequiredFactorHasDurationAndNotFactorGrantedAuthorityThenMissing() {
AllFactorsAuthorizationManager<Object> allFactors = AllFactorsAuthorizationManager.builder()
.requiredFactor(EXPIRING_PASSWORD)
.build();
Authentication authentication = new TestingAuthenticationToken("user", "password",
EXPIRING_PASSWORD.getAuthority());
FactorAuthorizationDecision result = allFactors.authorize(() -> authentication, DOES_NOT_MATTER);
assertThat(result.isGranted()).isFalse();
assertThat(result.getFactorErrors()).containsExactly(RequiredFactorError.createMissing(EXPIRING_PASSWORD));
}
@Test
void authorizeWhenFactorAuthorityMissingThenMissing() {
AllFactorsAuthorizationManager<Object> allFactors = AllFactorsAuthorizationManager.builder()
.requiredFactor(REQUIRED_PASSWORD)
.build();
Authentication authentication = new TestingAuthenticationToken("user", "password", "ROLE_USER");
FactorAuthorizationDecision result = allFactors.authorize(() -> authentication, DOES_NOT_MATTER);
assertThat(result.isGranted()).isFalse();
assertThat(result.getFactorErrors()).containsExactly(RequiredFactorError.createMissing(REQUIRED_PASSWORD));
}
@Test
void authorizeWhenFactorGrantedAuthorityMissingThenMissing() {
AllFactorsAuthorizationManager<Object> allFactors = AllFactorsAuthorizationManager.builder()
.requiredFactor(REQUIRED_PASSWORD)
.build();
Authentication authentication = new TestingAuthenticationToken("user", "password",
REQUIRED_PASSWORD.getAuthority());
FactorAuthorizationDecision result = allFactors.authorize(() -> authentication, DOES_NOT_MATTER);
assertThat(result.isGranted()).isFalse();
assertThat(result.getFactorErrors()).containsExactly(RequiredFactorError.createMissing(REQUIRED_PASSWORD));
}
@Test
void authorizeWhenExpired() {
AllFactorsAuthorizationManager<Object> allFactors = AllFactorsAuthorizationManager.builder()
.requiredFactor(EXPIRING_PASSWORD)
.build();
FactorGrantedAuthority passwordFactor = FactorGrantedAuthority.withAuthority(EXPIRING_PASSWORD.getAuthority())
.issuedAt(Instant.now().minus(Duration.ofHours(2)))
.build();
Authentication authentication = new TestingAuthenticationToken("user", "password", passwordFactor);
FactorAuthorizationDecision result = allFactors.authorize(() -> authentication, DOES_NOT_MATTER);
assertThat(result.isGranted()).isFalse();
assertThat(result.getFactorErrors()).containsExactly(RequiredFactorError.createExpired(EXPIRING_PASSWORD));
}
@Test
void authorizeWhenJustExpired() {
Instant now = Instant.now();
Duration expiresIn = Duration.ofHours(1);
Instant justExpired = now.minus(expiresIn);
Clock clock = Clock.fixed(now, ZoneId.systemDefault());
RequiredFactor expiringPassword = RequiredFactor.withAuthority(GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY)
.validDuration(expiresIn)
.build();
AllFactorsAuthorizationManager<Object> allFactors = AllFactorsAuthorizationManager.builder()
.requiredFactor(expiringPassword)
.build();
allFactors.setClock(clock);
FactorGrantedAuthority passwordFactor = FactorGrantedAuthority.withAuthority(expiringPassword.getAuthority())
.issuedAt(justExpired)
.build();
Authentication authentication = new TestingAuthenticationToken("user", "password", passwordFactor);
FactorAuthorizationDecision result = allFactors.authorize(() -> authentication, DOES_NOT_MATTER);
assertThat(result.isGranted()).isFalse();
assertThat(result.getFactorErrors()).containsExactly(RequiredFactorError.createExpired(expiringPassword));
}
@Test
void authorizeWhenAlmostExpired() {
Instant now = Instant.now();
Duration expiresIn = Duration.ofHours(1);
Instant justExpired = now.minus(expiresIn).plus(Duration.ofNanos(1));
Clock clock = Clock.fixed(now, ZoneId.systemDefault());
RequiredFactor expiringPassword = RequiredFactor.withAuthority(GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY)
.validDuration(expiresIn)
.build();
AllFactorsAuthorizationManager<Object> allFactors = AllFactorsAuthorizationManager.builder()
.requiredFactor(expiringPassword)
.build();
allFactors.setClock(clock);
FactorGrantedAuthority passwordFactor = FactorGrantedAuthority.withAuthority(expiringPassword.getAuthority())
.issuedAt(justExpired)
.build();
Authentication authentication = new TestingAuthenticationToken("user", "password", passwordFactor);
FactorAuthorizationDecision result = allFactors.authorize(() -> authentication, DOES_NOT_MATTER);
assertThat(result.isGranted()).isTrue();
}
@Test
void authorizeWhenDifferentFactorGrantedAuthorityThenMissing() {
AllFactorsAuthorizationManager<Object> allFactors = AllFactorsAuthorizationManager.builder()
.requiredFactor(REQUIRED_PASSWORD)
.build();
Authentication authentication = new TestingAuthenticationToken("user", "password",
FactorGrantedAuthority.fromAuthority(REQUIRED_PASSWORD.getAuthority()) + "DIFFERENT");
FactorAuthorizationDecision result = allFactors.authorize(() -> authentication, DOES_NOT_MATTER);
assertThat(result.isGranted()).isFalse();
assertThat(result.getFactorErrors()).containsExactly(RequiredFactorError.createMissing(REQUIRED_PASSWORD));
}
@Test
void setClockWhenNullThenIllegalArgumentException() {
AllFactorsAuthorizationManager<Object> allFactors = AllFactorsAuthorizationManager.builder()
.requiredFactor(REQUIRED_PASSWORD)
.build();
assertThatIllegalArgumentException().isThrownBy(() -> allFactors.setClock(null));
}
@Test
void builderBuildWhenEmpty() {
assertThatIllegalArgumentException().isThrownBy(() -> AllFactorsAuthorizationManager.builder().build());
}
@Test
void builderWhenNullRequiredFactor() {
AllFactorsAuthorizationManager.Builder builder = AllFactorsAuthorizationManager.builder();
assertThatIllegalArgumentException().isThrownBy(() -> builder.requiredFactor((RequiredFactor) null));
}
@Test
void builderWhenNullConsumerRequiredFactorBuilder() {
AllFactorsAuthorizationManager.Builder builder = AllFactorsAuthorizationManager.builder();
assertThatIllegalArgumentException()
.isThrownBy(() -> builder.requiredFactor((Consumer<RequiredFactor.Builder>) null));
}
}

View File

@ -0,0 +1,77 @@
/*
* Copyright 2004-present 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.authorization;
import java.util.Arrays;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.security.core.GrantedAuthorities;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link FactorAuthorizationDecision}.
*
* @author Rob Winch
* @since 7.0
*/
class FactorAuthorizationDecisionTests {
@Test
void isGrantedWhenEmptyThenTrue() {
FactorAuthorizationDecision decision = new FactorAuthorizationDecision(List.of());
assertThat(decision.isGranted()).isTrue();
}
@Test
void isGrantedWhenNotEmptyThenFalse() {
RequiredFactor requiredPassword = RequiredFactor.withAuthority(GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY)
.build();
RequiredFactorError missingPassword = RequiredFactorError.createMissing(requiredPassword);
FactorAuthorizationDecision decision = new FactorAuthorizationDecision(List.of(missingPassword));
assertThat(decision.isGranted()).isFalse();
}
@Test
void getFactorErrors() {
RequiredFactor requiredPassword = RequiredFactor.withAuthority(GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY)
.build();
RequiredFactorError missingPassword = RequiredFactorError.createMissing(requiredPassword);
List<RequiredFactorError> factorErrors = List.of(missingPassword);
FactorAuthorizationDecision decision = new FactorAuthorizationDecision(factorErrors);
assertThat(decision.getFactorErrors()).isEqualTo(factorErrors);
}
@Test
void constructorWhenNullThenThrowIllegalArgumentException() {
List<RequiredFactorError> factorErrors = null;
assertThatIllegalArgumentException().isThrownBy(() -> new FactorAuthorizationDecision(factorErrors));
}
@Test
void constructorWhenContainsNullThenThrowIllegalArgumentException() {
RequiredFactor requiredPassword = RequiredFactor.withAuthority(GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY)
.build();
RequiredFactorError missingPassword = RequiredFactorError.createMissing(requiredPassword);
List<RequiredFactorError> hasNullValue = Arrays.asList(missingPassword, null);
assertThatIllegalArgumentException().isThrownBy(() -> new FactorAuthorizationDecision(hasNullValue));
}
}

View File

@ -0,0 +1,64 @@
/*
* Copyright 2004-present 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.authorization;
import java.time.Duration;
import org.junit.jupiter.api.Test;
import org.springframework.security.core.GrantedAuthorities;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link RequiredFactorError}.
*
* @author Rob Winch
* @since 7.0
*/
class RequiredFactorErrorTests {
public static final RequiredFactor REQUIRED_FACTOR = RequiredFactor
.withAuthority(GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY)
.validDuration(Duration.ofHours(1))
.build();
@Test
void createMissing() {
RequiredFactorError error = RequiredFactorError.createMissing(REQUIRED_FACTOR);
assertThat(error.isMissing()).isTrue();
assertThat(error.isExpired()).isFalse();
assertThat(error.getRequiredFactor()).isEqualTo(REQUIRED_FACTOR);
}
@Test
void createExpired() {
RequiredFactorError error = RequiredFactorError.createExpired(REQUIRED_FACTOR);
assertThat(error.isMissing()).isFalse();
assertThat(error.isExpired()).isTrue();
assertThat(error.getRequiredFactor()).isEqualTo(REQUIRED_FACTOR);
}
@Test
void createExpiredWhenNullValidDurationThenIllegalArgumentException() {
RequiredFactor requiredPassword = RequiredFactor.withAuthority(GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY)
.build();
assertThatIllegalArgumentException().isThrownBy(() -> RequiredFactorError.createExpired(requiredPassword));
}
}

View File

@ -0,0 +1,60 @@
/*
* Copyright 2004-present 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.authorization;
import java.time.Duration;
import org.junit.jupiter.api.Test;
import org.springframework.security.core.GrantedAuthorities;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link RequiredFactor}.
*
* @author Rob Winch
* @since 7.0
*/
class RequiredFactorTests {
@Test
void builderWhenNullAuthorityIllegalArgumentException() {
RequiredFactor.Builder builder = RequiredFactor.builder();
assertThatIllegalArgumentException().isThrownBy(() -> builder.build());
}
@Test
void withAuthorityThenEquals() {
RequiredFactor requiredPassword = RequiredFactor.withAuthority(GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY)
.build();
assertThat(requiredPassword.getAuthority()).isEqualTo(GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY);
assertThat(requiredPassword.getValidDuration()).isNull();
}
@Test
void builderValidDurationThenEquals() {
Duration validDuration = Duration.ofMinutes(1);
RequiredFactor requiredPassword = RequiredFactor.withAuthority(GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY)
.validDuration(validDuration)
.build();
assertThat(requiredPassword.getAuthority()).isEqualTo(GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY);
assertThat(requiredPassword.getValidDuration()).isEqualTo(validDuration);
}
}