Skip to content

Commit 0618d4e

Browse files
Provide Runtime Hints for Beans used in Pre/PostAuthorize Expressions
Closes gh-14652
1 parent 61efede commit 0618d4e

File tree

5 files changed

+733
-0
lines changed

5 files changed

+733
-0
lines changed

config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java

+8
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
3636
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
3737
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
38+
import org.springframework.security.aot.hint.PrePostAuthorizeHintsRegistrar;
39+
import org.springframework.security.aot.hint.SecurityHintsRegistrar;
3840
import org.springframework.security.authorization.AuthorizationEventPublisher;
3941
import org.springframework.security.authorization.ObservationAuthorizationManager;
4042
import org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor;
@@ -191,6 +193,12 @@ static MethodInterceptor postFilterAuthorizationMethodInterceptor(
191193
() -> _prePostMethodSecurityConfiguration.getObject().postFilterMethodInterceptor);
192194
}
193195

196+
@Bean
197+
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
198+
static SecurityHintsRegistrar prePostAuthorizeExpressionHintsRegistrar() {
199+
return new PrePostAuthorizeHintsRegistrar();
200+
}
201+
194202
@Override
195203
public void setImportMetadata(AnnotationMetadata importMetadata) {
196204
EnableMethodSecurity annotation = importMetadata.getAnnotations().get(EnableMethodSecurity.class).synthesize();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/*
2+
* Copyright 2002-2024 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.aot.hint;
18+
19+
import java.lang.reflect.Method;
20+
import java.util.Arrays;
21+
import java.util.Collections;
22+
import java.util.HashSet;
23+
import java.util.List;
24+
import java.util.Set;
25+
26+
import org.springframework.aot.hint.MemberCategory;
27+
import org.springframework.aot.hint.RuntimeHints;
28+
import org.springframework.aot.hint.TypeReference;
29+
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
30+
import org.springframework.expression.spel.SpelNode;
31+
import org.springframework.expression.spel.ast.BeanReference;
32+
import org.springframework.expression.spel.standard.SpelExpression;
33+
import org.springframework.expression.spel.standard.SpelExpressionParser;
34+
import org.springframework.security.access.prepost.PostAuthorize;
35+
import org.springframework.security.access.prepost.PreAuthorize;
36+
import org.springframework.security.authorization.method.AuthorizeReturnObject;
37+
import org.springframework.security.core.annotation.SecurityAnnotationScanner;
38+
import org.springframework.security.core.annotation.SecurityAnnotationScanners;
39+
import org.springframework.util.Assert;
40+
41+
/**
42+
* A {@link SecurityHintsRegistrar} that scans all provided classes for methods that use
43+
* {@link PreAuthorize} or {@link PostAuthorize} and registers hints for the beans used
44+
* within the security expressions.
45+
*
46+
* <p>
47+
* It will also scan return types of methods annotated with {@link AuthorizeReturnObject}.
48+
*
49+
* <p>
50+
* This may be used by an application to register specific Security-adjacent classes that
51+
* were otherwise missed by Spring Security's reachability scans.
52+
*
53+
* <p>
54+
* Remember to register this as an infrastructural bean like so:
55+
*
56+
* <pre>
57+
* &#064;Bean
58+
* &#064;Role(BeanDefinition.ROLE_INFRASTRUCTURE)
59+
* static SecurityHintsRegistrar registerThese() {
60+
* return new PrePostAuthorizeExpressionBeanHintsRegistrar(MyClass.class);
61+
* }
62+
* </pre>
63+
*
64+
* @author Marcus da Coregio
65+
* @since 6.4
66+
* @see SecurityHintsAotProcessor
67+
*/
68+
public final class PrePostAuthorizeExpressionBeanHintsRegistrar implements SecurityHintsRegistrar {
69+
70+
private final SecurityAnnotationScanner<PreAuthorize> preAuthorizeScanner = SecurityAnnotationScanners
71+
.requireUnique(PreAuthorize.class);
72+
73+
private final SecurityAnnotationScanner<PostAuthorize> postAuthorizeScanner = SecurityAnnotationScanners
74+
.requireUnique(PostAuthorize.class);
75+
76+
private final SecurityAnnotationScanner<AuthorizeReturnObject> authorizeReturnObjectScanner = SecurityAnnotationScanners
77+
.requireUnique(AuthorizeReturnObject.class);
78+
79+
private final SpelExpressionParser expressionParser = new SpelExpressionParser();
80+
81+
private final Set<Class<?>> visitedClasses = new HashSet<>();
82+
83+
private final List<Class<?>> toVisit;
84+
85+
public PrePostAuthorizeExpressionBeanHintsRegistrar(Class<?>... toVisit) {
86+
this(Arrays.asList(toVisit));
87+
}
88+
89+
public PrePostAuthorizeExpressionBeanHintsRegistrar(List<Class<?>> toVisit) {
90+
Assert.notEmpty(toVisit, "toVisit cannot be empty");
91+
Assert.noNullElements(toVisit, "toVisit cannot contain null elements");
92+
this.toVisit = toVisit;
93+
}
94+
95+
@Override
96+
public void registerHints(RuntimeHints hints, ConfigurableListableBeanFactory beanFactory) {
97+
Set<String> expressions = new HashSet<>();
98+
for (Class<?> bean : this.toVisit) {
99+
expressions.addAll(extractSecurityExpressions(bean));
100+
}
101+
Set<String> beanNamesToRegister = new HashSet<>();
102+
for (String expression : expressions) {
103+
beanNamesToRegister.addAll(extractBeanNames(expression));
104+
}
105+
for (String toRegister : beanNamesToRegister) {
106+
Class<?> type = beanFactory.getType(toRegister, false);
107+
if (type == null) {
108+
continue;
109+
}
110+
hints.reflection().registerType(TypeReference.of(type), MemberCategory.INVOKE_DECLARED_METHODS);
111+
}
112+
}
113+
114+
private Set<String> extractSecurityExpressions(Class<?> clazz) {
115+
if (this.visitedClasses.contains(clazz)) {
116+
return Collections.emptySet();
117+
}
118+
this.visitedClasses.add(clazz);
119+
Set<String> expressions = new HashSet<>();
120+
for (Method method : clazz.getDeclaredMethods()) {
121+
PreAuthorize preAuthorize = this.preAuthorizeScanner.scan(method, clazz);
122+
PostAuthorize postAuthorize = this.postAuthorizeScanner.scan(method, clazz);
123+
if (preAuthorize != null) {
124+
expressions.add(preAuthorize.value());
125+
}
126+
if (postAuthorize != null) {
127+
expressions.add(postAuthorize.value());
128+
}
129+
AuthorizeReturnObject authorizeReturnObject = this.authorizeReturnObjectScanner.scan(method, clazz);
130+
if (authorizeReturnObject != null) {
131+
expressions.addAll(extractSecurityExpressions(method.getReturnType()));
132+
}
133+
}
134+
return expressions;
135+
}
136+
137+
private Set<String> extractBeanNames(String rawExpression) {
138+
SpelExpression expression = this.expressionParser.parseRaw(rawExpression);
139+
SpelNode node = expression.getAST();
140+
Set<String> beanNames = new HashSet<>();
141+
resolveBeanNames(beanNames, node);
142+
return beanNames;
143+
}
144+
145+
private void resolveBeanNames(Set<String> beanNames, SpelNode node) {
146+
if (node instanceof BeanReference br) {
147+
beanNames.add(br.getName());
148+
}
149+
int childCount = node.getChildCount();
150+
if (childCount == 0) {
151+
return;
152+
}
153+
for (int i = 0; i < childCount; i++) {
154+
resolveBeanNames(beanNames, node.getChild(i));
155+
}
156+
}
157+
158+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2002-2024 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.aot.hint;
18+
19+
import java.util.Arrays;
20+
import java.util.List;
21+
import java.util.stream.Collectors;
22+
23+
import org.springframework.aot.hint.RuntimeHints;
24+
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
25+
import org.springframework.beans.factory.support.RegisteredBean;
26+
import org.springframework.security.access.prepost.PostAuthorize;
27+
import org.springframework.security.access.prepost.PreAuthorize;
28+
29+
/**
30+
* A {@link SecurityHintsRegistrar} that scans all beans for methods that use
31+
* {@link PreAuthorize} or {@link PostAuthorize} and registers appropriate hints for the
32+
* annotations.
33+
*
34+
* @author Marcus da Coregio
35+
* @since 6.4
36+
* @see SecurityHintsAotProcessor
37+
* @see PrePostAuthorizeExpressionBeanHintsRegistrar
38+
*/
39+
public final class PrePostAuthorizeHintsRegistrar implements SecurityHintsRegistrar {
40+
41+
@Override
42+
public void registerHints(RuntimeHints hints, ConfigurableListableBeanFactory beanFactory) {
43+
List<Class<?>> beans = Arrays.stream(beanFactory.getBeanDefinitionNames())
44+
.map((beanName) -> RegisteredBean.of(beanFactory, beanName).getBeanClass())
45+
.collect(Collectors.toList());
46+
new PrePostAuthorizeExpressionBeanHintsRegistrar(beans).registerHints(hints, beanFactory);
47+
}
48+
49+
}

0 commit comments

Comments
 (0)