Skip to content

Commit d51006d

Browse files
Allow post-processing of authorization denied results with @PreAuthorize and @PostAuthorize
1 parent 62636b5 commit d51006d

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
@@ -662,6 +662,66 @@ public void methodWhenPostFilterMetaAnnotationThenFilters() {
662662
.containsExactly("dave");
663663
}
664664

665+
@Test
666+
@WithMockUser
667+
void getCardNumberWhenPostAuthorizeAndNotAdminThenReturnMasked() {
668+
this.spring
669+
.register(AuthzConfig.class, MethodSecurityServiceEnabledConfig.class,
670+
MethodSecurityService.CardNumberMaskingPostProcessor.class,
671+
MethodSecurityService.MaskingPostProcessor.class)
672+
.autowire();
673+
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
674+
String cardNumber = service.postAuthorizeGetCardNumberIfAdmin("4444-3333-2222-1111");
675+
assertThat(cardNumber).isEqualTo("****-****-****-1111");
676+
}
677+
678+
@Test
679+
@WithMockUser
680+
void getCardNumberWhenPreAuthorizeAndNotAdminThenReturnMasked() {
681+
this.spring
682+
.register(AuthzConfig.class, MethodSecurityServiceEnabledConfig.class,
683+
MethodSecurityService.MaskingPostProcessor.class)
684+
.autowire();
685+
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
686+
String cardNumber = service.preAuthorizeGetCardNumberIfAdmin("4444-3333-2222-1111");
687+
assertThat(cardNumber).isEqualTo("***");
688+
}
689+
690+
@Test
691+
@WithMockUser
692+
void getCardNumberWhenPreAuthorizeAndNotAdminAndChildHandlerThenResolveCorrectHandlerAndReturnMasked() {
693+
this.spring.register(AuthzConfig.class, MethodSecurityServiceEnabledConfig.class,
694+
MethodSecurityService.MaskingPostProcessor.class, MethodSecurityService.MaskingPostProcessorChild.class)
695+
.autowire();
696+
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
697+
String cardNumber = service.preAuthorizeWithHandlerChildGetCardNumberIfAdmin("4444-3333-2222-1111");
698+
assertThat(cardNumber).isEqualTo("***-child");
699+
}
700+
701+
@Test
702+
@WithMockUser(roles = "ADMIN")
703+
void preAuthorizeWhenHandlerAndAccessDeniedNotThrownFromPreAuthorizeThenNotHandled() {
704+
this.spring
705+
.register(AuthzConfig.class, MethodSecurityServiceEnabledConfig.class,
706+
MethodSecurityService.MaskingPostProcessor.class)
707+
.autowire();
708+
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
709+
assertThatExceptionOfType(AccessDeniedException.class)
710+
.isThrownBy(service::preAuthorizeThrowAccessDeniedManually);
711+
}
712+
713+
@Test
714+
@WithMockUser(roles = "ADMIN")
715+
void postAuthorizeWhenHandlerAndAccessDeniedNotThrownFromPostAuthorizeThenNotHandled() {
716+
this.spring
717+
.register(AuthzConfig.class, MethodSecurityServiceEnabledConfig.class,
718+
MethodSecurityService.PostMaskingPostProcessor.class)
719+
.autowire();
720+
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
721+
assertThatExceptionOfType(AccessDeniedException.class)
722+
.isThrownBy(service::postAuthorizeThrowAccessDeniedManually);
723+
}
724+
665725
private static Consumer<ConfigurableWebApplicationContext> disallowBeanOverriding() {
666726
return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false);
667727
}
@@ -675,6 +735,16 @@ private static Advisor returnAdvisor(int order) {
675735
return advisor;
676736
}
677737

738+
@Configuration
739+
static class AuthzConfig {
740+
741+
@Bean
742+
Authz authz() {
743+
return new Authz();
744+
}
745+
746+
}
747+
678748
@Configuration
679749
@EnableCustomMethodSecurity
680750
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)