Skip to content

Commit d62bd7b

Browse files
Allow post-processing of authorization denied results with @PreAuthorize and @PostAuthorize
1 parent ddab735 commit d62bd7b

File tree

35 files changed

+1978
-61
lines changed

35 files changed

+1978
-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

+212-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,42 @@
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.AuthorizeReturnObject;
43+
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
44+
import org.springframework.security.authorization.method.MethodAuthorizationDeniedPostProcessor;
45+
import org.springframework.security.authorization.method.MethodInvocationResult;
3046
import org.springframework.security.core.Authentication;
47+
import org.springframework.security.core.context.SecurityContextHolder;
3148
import org.springframework.security.core.parameters.P;
49+
import org.springframework.util.StringUtils;
3250

3351
/**
3452
* @author Rob Winch
3553
*/
54+
@MethodSecurityService.Mask("classmask")
3655
public interface MethodSecurityService {
3756

3857
@PreAuthorize("denyAll")
@@ -108,4 +127,196 @@ public interface MethodSecurityService {
108127
@RequireAdminRole
109128
void repeatedAnnotations();
110129

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

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

+72-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,74 @@ 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+
190+
@Override
191+
public UserRecordWithEmailProtected getUserRecordWithEmailProtected() {
192+
return new UserRecordWithEmailProtected("username", "[email protected]");
193+
}
194+
195+
@Override
196+
public UserRecordWithEmailProtected getUserWithFallbackWhenUnauthorized() {
197+
return new UserRecordWithEmailProtected("username", "[email protected]");
198+
}
199+
129200
}

0 commit comments

Comments
 (0)