Skip to content

Commit e0eff34

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

22 files changed

+617
-23
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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+
import org.springframework.context.annotation.Bean;
20+
import org.springframework.context.annotation.Configuration;
21+
import org.springframework.security.authorization.method.DefaultPostInvocationAuthorizationDeniedPostProcessor;
22+
import org.springframework.security.authorization.method.DefaultPreInvocationAuthorizationDeniedPostProcessor;
23+
24+
@Configuration(proxyBeanMethods = false)
25+
class AuthorizationPostProcessorConfiguration {
26+
27+
@Bean
28+
DefaultPreInvocationAuthorizationDeniedPostProcessor defaultPreAuthorizeMethodAccessDeniedHandler() {
29+
return new DefaultPreInvocationAuthorizationDeniedPostProcessor();
30+
}
31+
32+
@Bean
33+
DefaultPostInvocationAuthorizationDeniedPostProcessor defaultPostAuthorizeMethodAccessDeniedHandler() {
34+
return new DefaultPostInvocationAuthorizationDeniedPostProcessor();
35+
}
36+
37+
}

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

+1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ public String[] selectImports(@NonNull AnnotationMetadata importMetadata) {
5757
imports.add(Jsr250MethodSecurityConfiguration.class.getName());
5858
}
5959
imports.add(AuthorizationProxyConfiguration.class.getName());
60+
imports.add(AuthorizationPostProcessorConfiguration.class.getName());
6061
return imports.toArray(new String[0]);
6162
}
6263

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

+2
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ static MethodInterceptor preAuthorizeAuthorizationMethodInterceptor(
101101
AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
102102
.preAuthorize(manager(manager, registryProvider));
103103
preAuthorize.setOrder(preAuthorize.getOrder() + configuration.interceptorOrderOffset);
104+
preAuthorize.setApplicationContext(context);
104105
return new DeferringMethodInterceptor<>(preAuthorize, (f) -> {
105106
methodSecurityDefaultsProvider.ifAvailable(manager::setTemplateDefaults);
106107
manager.setExpressionHandler(expressionHandlerProvider
@@ -124,6 +125,7 @@ static MethodInterceptor postAuthorizeAuthorizationMethodInterceptor(
124125
AuthorizationManagerAfterMethodInterceptor postAuthorize = AuthorizationManagerAfterMethodInterceptor
125126
.postAuthorize(manager(manager, registryProvider));
126127
postAuthorize.setOrder(postAuthorize.getOrder() + configuration.interceptorOrderOffset);
128+
postAuthorize.setApplicationContext(context);
127129
return new DeferringMethodInterceptor<>(postAuthorize, (f) -> {
128130
methodSecurityDefaultsProvider.ifAvailable(manager::setTemplateDefaults);
129131
manager.setExpressionHandler(expressionHandlerProvider

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

+60-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.
@@ -21,12 +21,16 @@
2121
import jakarta.annotation.security.DenyAll;
2222
import jakarta.annotation.security.PermitAll;
2323
import jakarta.annotation.security.RolesAllowed;
24+
import org.aopalliance.intercept.MethodInvocation;
2425

2526
import org.springframework.security.access.annotation.Secured;
2627
import org.springframework.security.access.prepost.PostAuthorize;
2728
import org.springframework.security.access.prepost.PostFilter;
2829
import org.springframework.security.access.prepost.PreAuthorize;
2930
import org.springframework.security.access.prepost.PreFilter;
31+
import org.springframework.security.authorization.AuthorizationResult;
32+
import org.springframework.security.authorization.method.AuthorizationDeniedPostProcessor;
33+
import org.springframework.security.authorization.method.MethodInvocationResult;
3034
import org.springframework.security.core.Authentication;
3135
import org.springframework.security.core.parameters.P;
3236

@@ -108,4 +112,59 @@ public interface MethodSecurityService {
108112
@RequireAdminRole
109113
void repeatedAnnotations();
110114

115+
@PostAuthorize(value = "hasRole('ADMIN')", postProcessorClass = CardNumberMaskingPostProcessor.class)
116+
String postAuthorizeGetCardNumberIfAdmin(String cardNumber);
117+
118+
@PreAuthorize(value = "hasRole('ADMIN')", postProcessorClass = MaskingPostProcessor.class)
119+
String preAuthorizeGetCardNumberIfAdmin(String cardNumber);
120+
121+
@PreAuthorize(value = "hasRole('ADMIN')", postProcessorClass = MaskingPostProcessorChild.class)
122+
String preAuthorizeWithHandlerChildGetCardNumberIfAdmin(String cardNumber);
123+
124+
@PreAuthorize(value = "hasRole('ADMIN')", postProcessorClass = MaskingPostProcessor.class)
125+
String preAuthorizeThrowAccessDeniedManually();
126+
127+
@PostAuthorize(value = "hasRole('ADMIN')", postProcessorClass = PostMaskingPostProcessor.class)
128+
String postAuthorizeThrowAccessDeniedManually();
129+
130+
class MaskingPostProcessor implements AuthorizationDeniedPostProcessor<MethodInvocation> {
131+
132+
@Override
133+
public Object postProcessResult(MethodInvocation contextObject, AuthorizationResult result) {
134+
return "***";
135+
}
136+
137+
}
138+
139+
class MaskingPostProcessorChild extends MaskingPostProcessor {
140+
141+
@Override
142+
public Object postProcessResult(MethodInvocation contextObject, AuthorizationResult result) {
143+
Object mask = super.postProcessResult(contextObject, result);
144+
return mask + "-child";
145+
}
146+
147+
}
148+
149+
class PostMaskingPostProcessor implements AuthorizationDeniedPostProcessor<MethodInvocationResult> {
150+
151+
@Override
152+
public Object postProcessResult(MethodInvocationResult contextObject, AuthorizationResult result) {
153+
return "***";
154+
}
155+
156+
}
157+
158+
class CardNumberMaskingPostProcessor implements AuthorizationDeniedPostProcessor<MethodInvocationResult> {
159+
160+
static String MASK = "****-****-****-";
161+
162+
@Override
163+
public Object postProcessResult(MethodInvocationResult contextObject, AuthorizationResult result) {
164+
String cardNumber = (String) contextObject.getResult();
165+
return MASK + cardNumber.substring(cardNumber.length() - 4);
166+
}
167+
168+
}
169+
111170
}

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

+27-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,29 @@ 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+
129155
}

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

+70
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,66 @@ public void findAllWhenNestedPreAuthorizeThenAuthorizes() {
743743
});
744744
}
745745

746+
@Test
747+
@WithMockUser
748+
void getCardNumberWhenPostAuthorizeAndNotAdminThenReturnMasked() {
749+
this.spring
750+
.register(AuthzConfig.class, MethodSecurityServiceEnabledConfig.class,
751+
MethodSecurityService.CardNumberMaskingPostProcessor.class,
752+
MethodSecurityService.MaskingPostProcessor.class)
753+
.autowire();
754+
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
755+
String cardNumber = service.postAuthorizeGetCardNumberIfAdmin("4444-3333-2222-1111");
756+
assertThat(cardNumber).isEqualTo("****-****-****-1111");
757+
}
758+
759+
@Test
760+
@WithMockUser
761+
void getCardNumberWhenPreAuthorizeAndNotAdminThenReturnMasked() {
762+
this.spring
763+
.register(AuthzConfig.class, MethodSecurityServiceEnabledConfig.class,
764+
MethodSecurityService.MaskingPostProcessor.class)
765+
.autowire();
766+
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
767+
String cardNumber = service.preAuthorizeGetCardNumberIfAdmin("4444-3333-2222-1111");
768+
assertThat(cardNumber).isEqualTo("***");
769+
}
770+
771+
@Test
772+
@WithMockUser
773+
void getCardNumberWhenPreAuthorizeAndNotAdminAndChildHandlerThenResolveCorrectHandlerAndReturnMasked() {
774+
this.spring.register(AuthzConfig.class, MethodSecurityServiceEnabledConfig.class,
775+
MethodSecurityService.MaskingPostProcessor.class, MethodSecurityService.MaskingPostProcessorChild.class)
776+
.autowire();
777+
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
778+
String cardNumber = service.preAuthorizeWithHandlerChildGetCardNumberIfAdmin("4444-3333-2222-1111");
779+
assertThat(cardNumber).isEqualTo("***-child");
780+
}
781+
782+
@Test
783+
@WithMockUser(roles = "ADMIN")
784+
void preAuthorizeWhenHandlerAndAccessDeniedNotThrownFromPreAuthorizeThenNotHandled() {
785+
this.spring
786+
.register(AuthzConfig.class, MethodSecurityServiceEnabledConfig.class,
787+
MethodSecurityService.MaskingPostProcessor.class)
788+
.autowire();
789+
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
790+
assertThatExceptionOfType(AccessDeniedException.class)
791+
.isThrownBy(service::preAuthorizeThrowAccessDeniedManually);
792+
}
793+
794+
@Test
795+
@WithMockUser(roles = "ADMIN")
796+
void postAuthorizeWhenHandlerAndAccessDeniedNotThrownFromPostAuthorizeThenNotHandled() {
797+
this.spring
798+
.register(AuthzConfig.class, MethodSecurityServiceEnabledConfig.class,
799+
MethodSecurityService.PostMaskingPostProcessor.class)
800+
.autowire();
801+
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
802+
assertThatExceptionOfType(AccessDeniedException.class)
803+
.isThrownBy(service::postAuthorizeThrowAccessDeniedManually);
804+
}
805+
746806
private static Consumer<ConfigurableWebApplicationContext> disallowBeanOverriding() {
747807
return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false);
748808
}
@@ -756,6 +816,16 @@ private static Advisor returnAdvisor(int order) {
756816
return advisor;
757817
}
758818

819+
@Configuration
820+
static class AuthzConfig {
821+
822+
@Bean
823+
Authz authz() {
824+
return new Authz();
825+
}
826+
827+
}
828+
759829
@Configuration
760830
@EnableCustomMethodSecurity
761831
static class CustomMethodSecurityServiceConfig {

core/src/main/java/org/springframework/security/access/prepost/PostAuthorize.java

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2016 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.
@@ -23,6 +23,10 @@
2323
import java.lang.annotation.RetentionPolicy;
2424
import java.lang.annotation.Target;
2525

26+
import org.springframework.security.authorization.method.AuthorizationDeniedPostProcessor;
27+
import org.springframework.security.authorization.method.DefaultPostInvocationAuthorizationDeniedPostProcessor;
28+
import org.springframework.security.authorization.method.MethodInvocationResult;
29+
2630
/**
2731
* Annotation for specifying a method access-control expression which will be evaluated
2832
* after a method has been invoked.
@@ -42,4 +46,6 @@
4246
*/
4347
String value();
4448

49+
Class<? extends AuthorizationDeniedPostProcessor<MethodInvocationResult>> postProcessorClass() default DefaultPostInvocationAuthorizationDeniedPostProcessor.class;
50+
4551
}

core/src/main/java/org/springframework/security/access/prepost/PreAuthorize.java

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2016 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.
@@ -23,6 +23,11 @@
2323
import java.lang.annotation.RetentionPolicy;
2424
import java.lang.annotation.Target;
2525

26+
import org.aopalliance.intercept.MethodInvocation;
27+
28+
import org.springframework.security.authorization.method.AuthorizationDeniedPostProcessor;
29+
import org.springframework.security.authorization.method.DefaultPreInvocationAuthorizationDeniedPostProcessor;
30+
2631
/**
2732
* Annotation for specifying a method access-control expression which will be evaluated to
2833
* decide whether a method invocation is allowed or not.
@@ -42,4 +47,6 @@
4247
*/
4348
String value();
4449

50+
Class<? extends AuthorizationDeniedPostProcessor<MethodInvocation>> postProcessorClass() default DefaultPreInvocationAuthorizationDeniedPostProcessor.class;
51+
4552
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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.authorization;
18+
19+
import org.springframework.security.access.AccessDeniedException;
20+
import org.springframework.util.Assert;
21+
22+
public class AuthorizationException extends AccessDeniedException {
23+
24+
private final AuthorizationResult result;
25+
26+
public AuthorizationException(String msg, AuthorizationResult result) {
27+
super(msg);
28+
Assert.notNull(result, "decision cannot be null");
29+
Assert.state(!result.isGranted(), "Granted decisions are not supported");
30+
this.result = result;
31+
}
32+
33+
public AuthorizationResult getResult() {
34+
return this.result;
35+
}
36+
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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.authorization.method;
18+
19+
import org.springframework.lang.Nullable;
20+
import org.springframework.security.authorization.AuthorizationResult;
21+
22+
public interface AuthorizationDeniedPostProcessor<T> {
23+
24+
@Nullable
25+
Object postProcessResult(T contextObject, AuthorizationResult result);
26+
27+
}

0 commit comments

Comments
 (0)