Skip to content

Commit d1ff983

Browse files
committed
Add AllFactorsAuthorizationManager
Closes gh-17997
1 parent 3f74991 commit d1ff983

File tree

8 files changed

+968
-0
lines changed

8 files changed

+968
-0
lines changed
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
/*
2+
* Copyright 2004-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.authorization;
18+
19+
import java.time.Clock;
20+
import java.time.Instant;
21+
import java.util.ArrayList;
22+
import java.util.Collections;
23+
import java.util.List;
24+
import java.util.Objects;
25+
import java.util.Optional;
26+
import java.util.function.Consumer;
27+
import java.util.function.Supplier;
28+
import java.util.stream.Collectors;
29+
30+
import org.jspecify.annotations.Nullable;
31+
32+
import org.springframework.security.core.Authentication;
33+
import org.springframework.security.core.authority.FactorGrantedAuthority;
34+
import org.springframework.util.Assert;
35+
36+
/**
37+
* An {@link AuthorizationManager} that determines if the current user is authorized by
38+
* evaluating if the {@link Authentication} contains a {@link FactorGrantedAuthority} that
39+
* is not expired for each {@link RequiredFactor}.
40+
*
41+
* @author Rob Winch
42+
* @since 7.0
43+
* @see AuthorityAuthorizationManager
44+
*/
45+
public final class AllFactorsAuthorizationManager<T> implements AuthorizationManager<T> {
46+
47+
private Clock clock = Clock.systemUTC();
48+
49+
private final List<RequiredFactor> requiredFactors;
50+
51+
/**
52+
* Creates a new instance.
53+
* @param requiredFactors the authorities that are required.
54+
*/
55+
private AllFactorsAuthorizationManager(List<RequiredFactor> requiredFactors) {
56+
Assert.notEmpty(requiredFactors, "requiredFactors cannot be empty");
57+
Assert.noNullElements(requiredFactors, "requiredFactors must not contain null elements");
58+
this.requiredFactors = Collections.unmodifiableList(requiredFactors);
59+
}
60+
61+
/**
62+
* Sets the {@link Clock} to use.
63+
* @param clock the {@link Clock} to use. Cannot be null.
64+
*/
65+
public void setClock(Clock clock) {
66+
Assert.notNull(clock, "clock cannot be null");
67+
this.clock = clock;
68+
}
69+
70+
/**
71+
* For each {@link RequiredFactor} finds the first
72+
* {@link FactorGrantedAuthority#getAuthority()} that matches the
73+
* {@link RequiredFactor#getAuthority()}. The
74+
* {@link FactorGrantedAuthority#getIssuedAt()} must be more recent than
75+
* {@link RequiredFactor#getValidDuration()} (if non-null).
76+
* @param authentication the {@link Supplier} of the {@link Authentication} to check
77+
* @param object the object to check authorization on (not used).
78+
* @return an {@link FactorAuthorizationDecision}
79+
*/
80+
@Override
81+
public FactorAuthorizationDecision authorize(Supplier<? extends @Nullable Authentication> authentication,
82+
T object) {
83+
List<FactorGrantedAuthority> currentFactorAuthorities = getFactorGrantedAuthorities(authentication.get());
84+
List<RequiredFactorError> factorErrors = this.requiredFactors.stream()
85+
.map((factor) -> requiredFactorError(factor, currentFactorAuthorities))
86+
.filter(Objects::nonNull)
87+
.toList();
88+
return new FactorAuthorizationDecision(factorErrors);
89+
}
90+
91+
/**
92+
* Given the {@link RequiredFactor} and the current {@link FactorGrantedAuthority}
93+
* instances, returns {@link RequiredFactor} or null if granted.
94+
* @param requiredFactor the {@link RequiredFactor} to check.
95+
* @param currentFactors the current user's {@link FactorGrantedAuthority}.
96+
* @return the {@link RequiredFactor} or null if granted.
97+
*/
98+
private @Nullable RequiredFactorError requiredFactorError(RequiredFactor requiredFactor,
99+
List<FactorGrantedAuthority> currentFactors) {
100+
Optional<FactorGrantedAuthority> matchingAuthority = currentFactors.stream()
101+
.filter((authority) -> authority.getAuthority().equals(requiredFactor.getAuthority()))
102+
.findFirst();
103+
if (!matchingAuthority.isPresent()) {
104+
return RequiredFactorError.createMissing(requiredFactor);
105+
}
106+
return matchingAuthority.map((authority) -> {
107+
if (requiredFactor.getValidDuration() == null) {
108+
// granted (only requires authority to match)
109+
return null;
110+
}
111+
Instant now = this.clock.instant();
112+
Instant expiresAt = authority.getIssuedAt().plus(requiredFactor.getValidDuration());
113+
if (now.isBefore(expiresAt)) {
114+
// granted
115+
return null;
116+
}
117+
// denied (expired)
118+
return RequiredFactorError.createExpired(requiredFactor);
119+
}).orElse(null);
120+
}
121+
122+
/**
123+
* Extracts all of the {@link FactorGrantedAuthority} instances from
124+
* {@link Authentication#getAuthorities()}. If {@link Authentication} is null, or
125+
* {@link Authentication#isAuthenticated()} is false, then an empty {@link List} is
126+
* returned.
127+
* @param authentication the {@link Authentication} (possibly null).
128+
* @return all of the {@link FactorGrantedAuthority} instances from
129+
* {@link Authentication#getAuthorities()}.
130+
*/
131+
private List<FactorGrantedAuthority> getFactorGrantedAuthorities(@Nullable Authentication authentication) {
132+
if (authentication == null || !authentication.isAuthenticated()) {
133+
return Collections.emptyList();
134+
}
135+
// @formatter:off
136+
return authentication.getAuthorities().stream()
137+
.filter(FactorGrantedAuthority.class::isInstance)
138+
.map(FactorGrantedAuthority.class::cast)
139+
.collect(Collectors.toList());
140+
// @formatter:on
141+
}
142+
143+
/**
144+
* Creates a new {@link Builder}
145+
* @return
146+
*/
147+
public static Builder builder() {
148+
return new Builder();
149+
}
150+
151+
/**
152+
* A builder for {@link AllFactorsAuthorizationManager}.
153+
*
154+
* @author Rob Winch
155+
* @since 7.0
156+
*/
157+
public static final class Builder {
158+
159+
private List<RequiredFactor> requiredFactors = new ArrayList<>();
160+
161+
/**
162+
* Allows the user to consume the {@link RequiredFactor.Builder} that is passed in
163+
* and then adds the result to the {@link #requiredFactor(RequiredFactor)}.
164+
* @param requiredFactor the {@link Consumer} to invoke.
165+
* @return the builder.
166+
*/
167+
public Builder requiredFactor(Consumer<RequiredFactor.Builder> requiredFactor) {
168+
Assert.notNull(requiredFactor, "requiredFactor cannot be null");
169+
RequiredFactor.Builder builder = RequiredFactor.builder();
170+
requiredFactor.accept(builder);
171+
return requiredFactor(builder.build());
172+
}
173+
174+
/**
175+
* The {@link RequiredFactor} to add.
176+
* @param requiredFactor the requiredFactor to add. Cannot be null.
177+
* @return the builder.
178+
*/
179+
public Builder requiredFactor(RequiredFactor requiredFactor) {
180+
Assert.notNull(requiredFactor, "requiredFactor cannot be null");
181+
this.requiredFactors.add(requiredFactor);
182+
return this;
183+
}
184+
185+
/**
186+
* Builds the {@link AllFactorsAuthorizationManager}.
187+
* @param <T> the type.
188+
* @return the {@link AllFactorsAuthorizationManager}
189+
*/
190+
public <T> AllFactorsAuthorizationManager<T> build() {
191+
return new AllFactorsAuthorizationManager<T>(this.requiredFactors);
192+
}
193+
194+
}
195+
196+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright 2004-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.authorization;
18+
19+
import java.util.Collections;
20+
import java.util.List;
21+
22+
import org.springframework.util.Assert;
23+
24+
/**
25+
* An {@link AuthorizationResult} that contains {@link RequiredFactorError}.
26+
*
27+
* @author Rob Winch
28+
* @since 7.0
29+
*/
30+
public class FactorAuthorizationDecision implements AuthorizationResult {
31+
32+
private final List<RequiredFactorError> factorErrors;
33+
34+
/**
35+
* Creates a new instance.
36+
* @param factorErrors the {@link RequiredFactorError}. If empty, {@link #isGranted()}
37+
* returns true. Cannot be null or contain empty values.
38+
*/
39+
public FactorAuthorizationDecision(List<RequiredFactorError> factorErrors) {
40+
Assert.notNull(factorErrors, "factorErrors cannot be null");
41+
Assert.noNullElements(factorErrors, "factorErrors must not contain null elements");
42+
this.factorErrors = Collections.unmodifiableList(factorErrors);
43+
}
44+
45+
/**
46+
* The specified {@link RequiredFactorError}s
47+
* @return the errors. Cannot be null or contain null values.
48+
*/
49+
public List<RequiredFactorError> getFactorErrors() {
50+
return this.factorErrors;
51+
}
52+
53+
/**
54+
* Returns {@code getFactorErrors().isEmpty()}.
55+
* @return
56+
*/
57+
@Override
58+
public boolean isGranted() {
59+
return this.factorErrors.isEmpty();
60+
}
61+
62+
}

0 commit comments

Comments
 (0)