Skip to content

Commit 4142ce0

Browse files
Allow post-processing of authorization denied results with @PreAuthorize and @PostAuthorize
1 parent a9cdaf3 commit 4142ce0

File tree

34 files changed

+1861
-61
lines changed

34 files changed

+1861
-61
lines changed

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.

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

+2
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ static MethodInterceptor preAuthorizeAuthorizationMethodInterceptor(
9898
ObjectProvider<ObservationRegistry> registryProvider, ObjectProvider<RoleHierarchy> roleHierarchyProvider,
9999
PrePostMethodSecurityConfiguration configuration, ApplicationContext context) {
100100
PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager();
101+
manager.setApplicationContext(context);
101102
AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
102103
.preAuthorize(manager(manager, registryProvider));
103104
preAuthorize.setOrder(preAuthorize.getOrder() + configuration.interceptorOrderOffset);
@@ -121,6 +122,7 @@ static MethodInterceptor postAuthorizeAuthorizationMethodInterceptor(
121122
ObjectProvider<ObservationRegistry> registryProvider, ObjectProvider<RoleHierarchy> roleHierarchyProvider,
122123
PrePostMethodSecurityConfiguration configuration, ApplicationContext context) {
123124
PostAuthorizeAuthorizationManager manager = new PostAuthorizeAuthorizationManager();
125+
manager.setApplicationContext(context);
124126
AuthorizationManagerAfterMethodInterceptor postAuthorize = AuthorizationManagerAfterMethodInterceptor
125127
.postAuthorize(manager(manager, registryProvider));
126128
postAuthorize.setOrder(postAuthorize.getOrder() + configuration.interceptorOrderOffset);

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

+5-2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.springframework.beans.factory.ObjectProvider;
3232
import org.springframework.beans.factory.annotation.Autowired;
3333
import org.springframework.beans.factory.config.BeanDefinition;
34+
import org.springframework.context.ApplicationContext;
3435
import org.springframework.context.annotation.Bean;
3536
import org.springframework.context.annotation.Configuration;
3637
import org.springframework.context.annotation.Role;
@@ -74,9 +75,10 @@ static MethodInterceptor preFilterAuthorizationMethodInterceptor(MethodSecurityE
7475
static MethodInterceptor preAuthorizeAuthorizationMethodInterceptor(
7576
MethodSecurityExpressionHandler expressionHandler,
7677
ObjectProvider<PrePostTemplateDefaults> defaultsObjectProvider,
77-
ObjectProvider<ObservationRegistry> registryProvider) {
78+
ObjectProvider<ObservationRegistry> registryProvider, ApplicationContext context) {
7879
PreAuthorizeReactiveAuthorizationManager manager = new PreAuthorizeReactiveAuthorizationManager(
7980
expressionHandler);
81+
manager.setApplicationContext(context);
8082
ReactiveAuthorizationManager<MethodInvocation> authorizationManager = manager(manager, registryProvider);
8183
AuthorizationAdvisor interceptor = AuthorizationManagerBeforeReactiveMethodInterceptor
8284
.preAuthorize(authorizationManager);
@@ -99,9 +101,10 @@ static MethodInterceptor postFilterAuthorizationMethodInterceptor(MethodSecurity
99101
static MethodInterceptor postAuthorizeAuthorizationMethodInterceptor(
100102
MethodSecurityExpressionHandler expressionHandler,
101103
ObjectProvider<PrePostTemplateDefaults> defaultsObjectProvider,
102-
ObjectProvider<ObservationRegistry> registryProvider) {
104+
ObjectProvider<ObservationRegistry> registryProvider, ApplicationContext context) {
103105
PostAuthorizeReactiveAuthorizationManager manager = new PostAuthorizeReactiveAuthorizationManager(
104106
expressionHandler);
107+
manager.setApplicationContext(context);
105108
ReactiveAuthorizationManager<MethodInvocationResult> authorizationManager = manager(manager, registryProvider);
106109
AuthorizationAdvisor interceptor = AuthorizationManagerAfterReactiveMethodInterceptor
107110
.postAuthorize(authorizationManager);

config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityService.java

+193-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,23 +16,41 @@
1616

1717
package org.springframework.security.config.annotation.method.configuration;
1818

19+
import java.lang.annotation.ElementType;
20+
import java.lang.annotation.Inherited;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
1924
import java.util.List;
2025

2126
import jakarta.annotation.security.DenyAll;
2227
import jakarta.annotation.security.PermitAll;
2328
import jakarta.annotation.security.RolesAllowed;
29+
import org.aopalliance.intercept.MethodInvocation;
2430

31+
import org.springframework.context.ApplicationContext;
32+
import org.springframework.core.annotation.AnnotationUtils;
33+
import org.springframework.expression.EvaluationContext;
34+
import org.springframework.expression.Expression;
2535
import org.springframework.security.access.annotation.Secured;
36+
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
2637
import org.springframework.security.access.prepost.PostAuthorize;
2738
import org.springframework.security.access.prepost.PostFilter;
2839
import org.springframework.security.access.prepost.PreAuthorize;
2940
import org.springframework.security.access.prepost.PreFilter;
41+
import org.springframework.security.authorization.AuthorizationResult;
42+
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
43+
import org.springframework.security.authorization.method.MethodAuthorizationDeniedPostProcessor;
44+
import org.springframework.security.authorization.method.MethodInvocationResult;
3045
import org.springframework.security.core.Authentication;
46+
import org.springframework.security.core.context.SecurityContextHolder;
3147
import org.springframework.security.core.parameters.P;
48+
import org.springframework.util.StringUtils;
3249

3350
/**
3451
* @author Rob Winch
3552
*/
53+
@MethodSecurityService.Mask("classmask")
3654
public interface MethodSecurityService {
3755

3856
@PreAuthorize("denyAll")
@@ -108,4 +126,178 @@ public interface MethodSecurityService {
108126
@RequireAdminRole
109127
void repeatedAnnotations();
110128

129+
@PreAuthorize(value = "hasRole('ADMIN')", handlerClass = StarMaskingHandler.class)
130+
String preAuthorizeGetCardNumberIfAdmin(String cardNumber);
131+
132+
@PreAuthorize(value = "hasRole('ADMIN')", handlerClass = StartMaskingHandlerChild.class)
133+
String preAuthorizeWithHandlerChildGetCardNumberIfAdmin(String cardNumber);
134+
135+
@PreAuthorize(value = "hasRole('ADMIN')", handlerClass = StarMaskingHandler.class)
136+
String preAuthorizeThrowAccessDeniedManually();
137+
138+
@PostAuthorize(value = "hasRole('ADMIN')", postProcessorClass = CardNumberMaskingPostProcessor.class)
139+
String postAuthorizeGetCardNumberIfAdmin(String cardNumber);
140+
141+
@PostAuthorize(value = "hasRole('ADMIN')", postProcessorClass = PostMaskingPostProcessor.class)
142+
String postAuthorizeThrowAccessDeniedManually();
143+
144+
@PreAuthorize(value = "denyAll()", handlerClass = MaskAnnotationHandler.class)
145+
@Mask("methodmask")
146+
String preAuthorizeDeniedMethodWithMaskAnnotation();
147+
148+
@PreAuthorize(value = "denyAll()", handlerClass = MaskAnnotationHandler.class)
149+
String preAuthorizeDeniedMethodWithNoMaskAnnotation();
150+
151+
@NullDenied(role = "ADMIN")
152+
String postAuthorizeDeniedWithNullDenied();
153+
154+
@PostAuthorize(value = "denyAll()", postProcessorClass = MaskAnnotationPostProcessor.class)
155+
@Mask("methodmask")
156+
String postAuthorizeDeniedMethodWithMaskAnnotation();
157+
158+
@PostAuthorize(value = "denyAll()", postProcessorClass = MaskAnnotationPostProcessor.class)
159+
String postAuthorizeDeniedMethodWithNoMaskAnnotation();
160+
161+
@PreAuthorize(value = "hasRole('ADMIN')", handlerClass = MaskAnnotationHandler.class)
162+
@Mask(expression = "@myMasker.getMask()")
163+
String preAuthorizeWithMaskAnnotationUsingBean();
164+
165+
@PostAuthorize(value = "hasRole('ADMIN')", postProcessorClass = MaskAnnotationPostProcessor.class)
166+
@Mask(expression = "@myMasker.getMask(returnObject)")
167+
String postAuthorizeWithMaskAnnotationUsingBean();
168+
169+
class StarMaskingHandler implements MethodAuthorizationDeniedHandler {
170+
171+
@Override
172+
public Object handle(MethodInvocation methodInvocation, AuthorizationResult result) {
173+
return "***";
174+
}
175+
176+
}
177+
178+
class StartMaskingHandlerChild extends StarMaskingHandler {
179+
180+
@Override
181+
public Object handle(MethodInvocation methodInvocation, AuthorizationResult result) {
182+
return super.handle(methodInvocation, result) + "-child";
183+
}
184+
185+
}
186+
187+
class MaskAnnotationHandler implements MethodAuthorizationDeniedHandler {
188+
189+
MaskValueResolver maskValueResolver;
190+
191+
MaskAnnotationHandler(ApplicationContext context) {
192+
this.maskValueResolver = new MaskValueResolver(context);
193+
}
194+
195+
@Override
196+
public Object handle(MethodInvocation methodInvocation, AuthorizationResult result) {
197+
Mask mask = AnnotationUtils.getAnnotation(methodInvocation.getMethod(), Mask.class);
198+
if (mask == null) {
199+
mask = AnnotationUtils.getAnnotation(methodInvocation.getMethod().getDeclaringClass(), Mask.class);
200+
}
201+
return this.maskValueResolver.resolveValue(mask, methodInvocation, null);
202+
}
203+
204+
}
205+
206+
class MaskAnnotationPostProcessor implements MethodAuthorizationDeniedPostProcessor {
207+
208+
MaskValueResolver maskValueResolver;
209+
210+
MaskAnnotationPostProcessor(ApplicationContext context) {
211+
this.maskValueResolver = new MaskValueResolver(context);
212+
}
213+
214+
@Override
215+
public Object postProcessResult(MethodInvocationResult methodInvocationResult,
216+
AuthorizationResult authorizationResult) {
217+
MethodInvocation mi = methodInvocationResult.getMethodInvocation();
218+
Mask mask = AnnotationUtils.getAnnotation(mi.getMethod(), Mask.class);
219+
if (mask == null) {
220+
mask = AnnotationUtils.getAnnotation(mi.getMethod().getDeclaringClass(), Mask.class);
221+
}
222+
return this.maskValueResolver.resolveValue(mask, mi, methodInvocationResult.getResult());
223+
}
224+
225+
}
226+
227+
class MaskValueResolver {
228+
229+
DefaultMethodSecurityExpressionHandler expressionHandler;
230+
231+
MaskValueResolver(ApplicationContext context) {
232+
this.expressionHandler = new DefaultMethodSecurityExpressionHandler();
233+
this.expressionHandler.setApplicationContext(context);
234+
}
235+
236+
String resolveValue(Mask mask, MethodInvocation mi, Object returnObject) {
237+
if (StringUtils.hasText(mask.value())) {
238+
return mask.value();
239+
}
240+
Expression expression = this.expressionHandler.getExpressionParser().parseExpression(mask.expression());
241+
EvaluationContext evaluationContext = this.expressionHandler
242+
.createEvaluationContext(() -> SecurityContextHolder.getContext().getAuthentication(), mi);
243+
if (returnObject != null) {
244+
this.expressionHandler.setReturnObject(returnObject, evaluationContext);
245+
}
246+
return expression.getValue(evaluationContext, String.class);
247+
}
248+
249+
}
250+
251+
class PostMaskingPostProcessor implements MethodAuthorizationDeniedPostProcessor {
252+
253+
@Override
254+
public Object postProcessResult(MethodInvocationResult contextObject, AuthorizationResult result) {
255+
return "***";
256+
}
257+
258+
}
259+
260+
class CardNumberMaskingPostProcessor implements MethodAuthorizationDeniedPostProcessor {
261+
262+
static String MASK = "****-****-****-";
263+
264+
@Override
265+
public Object postProcessResult(MethodInvocationResult contextObject, AuthorizationResult result) {
266+
String cardNumber = (String) contextObject.getResult();
267+
return MASK + cardNumber.substring(cardNumber.length() - 4);
268+
}
269+
270+
}
271+
272+
class NullPostProcessor implements MethodAuthorizationDeniedPostProcessor {
273+
274+
@Override
275+
public Object postProcessResult(MethodInvocationResult methodInvocationResult,
276+
AuthorizationResult authorizationResult) {
277+
return null;
278+
}
279+
280+
}
281+
282+
@Target({ ElementType.METHOD, ElementType.TYPE })
283+
@Retention(RetentionPolicy.RUNTIME)
284+
@Inherited
285+
@interface Mask {
286+
287+
String value() default "";
288+
289+
String expression() default "";
290+
291+
}
292+
293+
@Target({ ElementType.METHOD, ElementType.TYPE })
294+
@Retention(RetentionPolicy.RUNTIME)
295+
@Inherited
296+
@PostAuthorize(value = "hasRole('{value}')", postProcessorClass = NullPostProcessor.class)
297+
@interface NullDenied {
298+
299+
String role();
300+
301+
}
302+
111303
}

config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityServiceImpl.java

+62-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@
1818

1919
import java.util.List;
2020

21+
import org.springframework.security.access.AccessDeniedException;
2122
import org.springframework.security.core.Authentication;
2223
import org.springframework.security.core.context.SecurityContextHolder;
2324

@@ -126,4 +127,64 @@ public List<String> allAnnotations(List<String> list) {
126127
public void repeatedAnnotations() {
127128
}
128129

130+
@Override
131+
public String postAuthorizeGetCardNumberIfAdmin(String cardNumber) {
132+
return cardNumber;
133+
}
134+
135+
@Override
136+
public String preAuthorizeGetCardNumberIfAdmin(String cardNumber) {
137+
return cardNumber;
138+
}
139+
140+
@Override
141+
public String preAuthorizeWithHandlerChildGetCardNumberIfAdmin(String cardNumber) {
142+
return cardNumber;
143+
}
144+
145+
@Override
146+
public String preAuthorizeThrowAccessDeniedManually() {
147+
throw new AccessDeniedException("Access Denied");
148+
}
149+
150+
@Override
151+
public String postAuthorizeThrowAccessDeniedManually() {
152+
throw new AccessDeniedException("Access Denied");
153+
}
154+
155+
@Override
156+
public String preAuthorizeDeniedMethodWithMaskAnnotation() {
157+
return "ok";
158+
}
159+
160+
@Override
161+
public String preAuthorizeDeniedMethodWithNoMaskAnnotation() {
162+
return "ok";
163+
}
164+
165+
@Override
166+
public String postAuthorizeDeniedWithNullDenied() {
167+
return "ok";
168+
}
169+
170+
@Override
171+
public String postAuthorizeDeniedMethodWithMaskAnnotation() {
172+
return "ok";
173+
}
174+
175+
@Override
176+
public String postAuthorizeDeniedMethodWithNoMaskAnnotation() {
177+
return "ok";
178+
}
179+
180+
@Override
181+
public String preAuthorizeWithMaskAnnotationUsingBean() {
182+
return "ok";
183+
}
184+
185+
@Override
186+
public String postAuthorizeWithMaskAnnotationUsingBean() {
187+
return "ok";
188+
}
189+
129190
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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.config.annotation.method.configuration;
18+
19+
public class MyMasker {
20+
21+
public String getMask(String value) {
22+
return value + "-masked";
23+
}
24+
25+
public String getMask() {
26+
return "mask";
27+
}
28+
29+
}

0 commit comments

Comments
 (0)