Skip to content

Commit 548af57

Browse files
Add support for handling access denied with @PreAuthorize and @PostAuthorize
1 parent f82e15d commit 548af57

16 files changed

+510
-14
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.DefaultPostInvocationMethodAccessDeniedHandler;
22+
import org.springframework.security.authorization.method.DefaultPreInvocationMethodAccessDeniedHandler;
23+
24+
@Configuration(proxyBeanMethods = false)
25+
class MethodAccessDeniedHandlerConfiguration {
26+
27+
@Bean
28+
DefaultPreInvocationMethodAccessDeniedHandler defaultPreAuthorizeMethodAccessDeniedHandler() {
29+
return new DefaultPreInvocationMethodAccessDeniedHandler();
30+
}
31+
32+
@Bean
33+
DefaultPostInvocationMethodAccessDeniedHandler defaultPostAuthorizeMethodAccessDeniedHandler() {
34+
return new DefaultPostInvocationMethodAccessDeniedHandler();
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(MethodAccessDeniedHandlerConfiguration.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.AuthorizationDecision;
32+
import org.springframework.security.authorization.method.MethodAccessDeniedHandler;
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')", handlerClass = CardNumberMaskingHandler.class)
116+
String postAuthorizeGetCardNumberIfAdmin(String cardNumber);
117+
118+
@PreAuthorize(value = "hasRole('ADMIN')", handlerClass = MaskingHandler.class)
119+
String preAuthorizeGetCardNumberIfAdmin(String cardNumber);
120+
121+
@PreAuthorize(value = "hasRole('ADMIN')", handlerClass = MaskingHandlerChild.class)
122+
String preAuthorizeWithHandlerChildGetCardNumberIfAdmin(String cardNumber);
123+
124+
@PreAuthorize(value = "hasRole('ADMIN')", handlerClass = MaskingHandler.class)
125+
String preAuthorizeThrowAccessDeniedManually();
126+
127+
@PostAuthorize(value = "hasRole('ADMIN')", handlerClass = PostMaskingHandler.class)
128+
String postAuthorizeThrowAccessDeniedManually();
129+
130+
class MaskingHandler implements MethodAccessDeniedHandler<MethodInvocation> {
131+
132+
@Override
133+
public Object handle(MethodInvocation deniedObject, AuthorizationDecision decision) {
134+
return "***";
135+
}
136+
137+
}
138+
139+
class MaskingHandlerChild extends MaskingHandler {
140+
141+
@Override
142+
public Object handle(MethodInvocation deniedObject, AuthorizationDecision decision) {
143+
Object mask = super.handle(deniedObject, decision);
144+
return mask + "-child";
145+
}
146+
147+
}
148+
149+
class PostMaskingHandler implements MethodAccessDeniedHandler<MethodInvocationResult> {
150+
151+
@Override
152+
public Object handle(MethodInvocationResult deniedObject, AuthorizationDecision decision) {
153+
return "***";
154+
}
155+
156+
}
157+
158+
class CardNumberMaskingHandler implements MethodAccessDeniedHandler<MethodInvocationResult> {
159+
160+
static String MASK = "****-****-****-";
161+
162+
@Override
163+
public Object handle(MethodInvocationResult mi, AuthorizationDecision decision) {
164+
String cardNumber = (String) mi.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.CardNumberMaskingHandler.class, MethodSecurityService.MaskingHandler.class)
671+
.autowire();
672+
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
673+
String cardNumber = service.postAuthorizeGetCardNumberIfAdmin("4444-3333-2222-1111");
674+
assertThat(cardNumber).isEqualTo("****-****-****-1111");
675+
}
676+
677+
@Test
678+
@WithMockUser
679+
void getCardNumberWhenPreAuthorizeAndNotAdminThenReturnMasked() {
680+
this.spring
681+
.register(AuthzConfig.class, MethodSecurityServiceEnabledConfig.class,
682+
MethodSecurityService.MaskingHandler.class)
683+
.autowire();
684+
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
685+
String cardNumber = service.preAuthorizeGetCardNumberIfAdmin("4444-3333-2222-1111");
686+
assertThat(cardNumber).isEqualTo("***");
687+
}
688+
689+
@Test
690+
@WithMockUser
691+
void getCardNumberWhenPreAuthorizeAndNotAdminAndChildHandlerThenResolveCorrectHandlerAndReturnMasked() {
692+
this.spring
693+
.register(AuthzConfig.class, MethodSecurityServiceEnabledConfig.class,
694+
MethodSecurityService.MaskingHandler.class, MethodSecurityService.MaskingHandlerChild.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.MaskingHandler.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.PostMaskingHandler.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.DefaultPostInvocationMethodAccessDeniedHandler;
27+
import org.springframework.security.authorization.method.MethodAccessDeniedHandler;
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 MethodAccessDeniedHandler<MethodInvocationResult>> handlerClass() default DefaultPostInvocationMethodAccessDeniedHandler.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.DefaultPreInvocationMethodAccessDeniedHandler;
29+
import org.springframework.security.authorization.method.MethodAccessDeniedHandler;
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 MethodAccessDeniedHandler<MethodInvocation>> handlerClass() default DefaultPreInvocationMethodAccessDeniedHandler.class;
51+
4552
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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 DecisionAwareAccessDeniedException extends AccessDeniedException {
23+
24+
private final AuthorizationDecision decision;
25+
26+
public DecisionAwareAccessDeniedException(String msg, AuthorizationDecision decision) {
27+
super(msg);
28+
Assert.notNull(decision, "decision cannot be null");
29+
this.decision = decision;
30+
}
31+
32+
public AuthorizationDecision getDecision() {
33+
return this.decision;
34+
}
35+
36+
}

0 commit comments

Comments
 (0)