Skip to content

Commit f2412f4

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

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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 60 additions & 1 deletion
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

Lines changed: 27 additions & 1 deletion
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

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -740,6 +740,66 @@ public void findAllWhenNestedPreAuthorizeThenAuthorizes() {
740740
});
741741
}
742742

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

816+
@Configuration
817+
static class AuthzConfig {
818+
819+
@Bean
820+
Authz authz() {
821+
return new Authz();
822+
}
823+
824+
}
825+
756826
@Configuration
757827
@EnableCustomMethodSecurity
758828
static class CustomMethodSecurityServiceConfig {

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

Lines changed: 7 additions & 1 deletion
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

Lines changed: 8 additions & 1 deletion
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
}
Lines changed: 37 additions & 0 deletions
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)