From a7930b5df001d4798b80a0a846ef0c41fa8056b3 Mon Sep 17 00:00:00 2001 From: Marcus Hert Da Coregio Date: Fri, 8 Mar 2024 10:46:27 -0300 Subject: [PATCH] Add support for handling access denied with @PreAuthorize and @PostAuthorize --- ...ethodAccessDeniedHandlerConfiguration.java | 37 ++++++++++ .../configuration/MethodSecuritySelector.java | 1 + .../PrePostMethodSecurityConfiguration.java | 2 + .../configuration/MethodSecurityService.java | 61 +++++++++++++++- .../MethodSecurityServiceImpl.java | 28 +++++++- ...ePostMethodSecurityConfigurationTests.java | 70 +++++++++++++++++++ .../access/prepost/PostAuthorize.java | 8 ++- .../security/access/prepost/PreAuthorize.java | 9 ++- ...rizationDecisionAccessDeniedException.java | 36 ++++++++++ ...rizationManagerAfterMethodInterceptor.java | 42 +++++++++-- ...izationManagerBeforeMethodInterceptor.java | 42 +++++++++-- ...aultMethodAccessDeniedHandlerResolver.java | 70 +++++++++++++++++++ ...stInvocationMethodAccessDeniedHandler.java | 30 ++++++++ ...reInvocationMethodAccessDeniedHandler.java | 31 ++++++++ .../method/MethodAccessDeniedHandler.java | 28 ++++++++ .../MethodAccessDeniedHandlerResolver.java | 29 ++++++++ 16 files changed, 510 insertions(+), 14 deletions(-) create mode 100644 config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodAccessDeniedHandlerConfiguration.java create mode 100644 core/src/main/java/org/springframework/security/authorization/AuthorizationDecisionAccessDeniedException.java create mode 100644 core/src/main/java/org/springframework/security/authorization/method/DefaultMethodAccessDeniedHandlerResolver.java create mode 100644 core/src/main/java/org/springframework/security/authorization/method/DefaultPostInvocationMethodAccessDeniedHandler.java create mode 100644 core/src/main/java/org/springframework/security/authorization/method/DefaultPreInvocationMethodAccessDeniedHandler.java create mode 100644 core/src/main/java/org/springframework/security/authorization/method/MethodAccessDeniedHandler.java create mode 100644 core/src/main/java/org/springframework/security/authorization/method/MethodAccessDeniedHandlerResolver.java diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodAccessDeniedHandlerConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodAccessDeniedHandlerConfiguration.java new file mode 100644 index 00000000000..9c7dbbe088e --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodAccessDeniedHandlerConfiguration.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.method.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authorization.method.DefaultPostInvocationMethodAccessDeniedHandler; +import org.springframework.security.authorization.method.DefaultPreInvocationMethodAccessDeniedHandler; + +@Configuration(proxyBeanMethods = false) +class MethodAccessDeniedHandlerConfiguration { + + @Bean + DefaultPreInvocationMethodAccessDeniedHandler defaultPreAuthorizeMethodAccessDeniedHandler() { + return new DefaultPreInvocationMethodAccessDeniedHandler(); + } + + @Bean + DefaultPostInvocationMethodAccessDeniedHandler defaultPostAuthorizeMethodAccessDeniedHandler() { + return new DefaultPostInvocationMethodAccessDeniedHandler(); + } + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodSecuritySelector.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodSecuritySelector.java index 928ed485484..4771011b4b0 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodSecuritySelector.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodSecuritySelector.java @@ -57,6 +57,7 @@ public String[] selectImports(@NonNull AnnotationMetadata importMetadata) { imports.add(Jsr250MethodSecurityConfiguration.class.getName()); } imports.add(AuthorizationProxyConfiguration.class.getName()); + imports.add(MethodAccessDeniedHandlerConfiguration.class.getName()); return imports.toArray(new String[0]); } diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java index 7fea76850df..f84dac1b30a 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java @@ -101,6 +101,7 @@ static MethodInterceptor preAuthorizeAuthorizationMethodInterceptor( AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor .preAuthorize(manager(manager, registryProvider)); preAuthorize.setOrder(preAuthorize.getOrder() + configuration.interceptorOrderOffset); + preAuthorize.setApplicationContext(context); return new DeferringMethodInterceptor<>(preAuthorize, (f) -> { methodSecurityDefaultsProvider.ifAvailable(manager::setTemplateDefaults); manager.setExpressionHandler(expressionHandlerProvider @@ -124,6 +125,7 @@ static MethodInterceptor postAuthorizeAuthorizationMethodInterceptor( AuthorizationManagerAfterMethodInterceptor postAuthorize = AuthorizationManagerAfterMethodInterceptor .postAuthorize(manager(manager, registryProvider)); postAuthorize.setOrder(postAuthorize.getOrder() + configuration.interceptorOrderOffset); + postAuthorize.setApplicationContext(context); return new DeferringMethodInterceptor<>(postAuthorize, (f) -> { methodSecurityDefaultsProvider.ifAvailable(manager::setTemplateDefaults); manager.setExpressionHandler(expressionHandlerProvider diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityService.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityService.java index 00451610ed3..a3debc6b944 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityService.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityService.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,12 +21,16 @@ import jakarta.annotation.security.DenyAll; import jakarta.annotation.security.PermitAll; import jakarta.annotation.security.RolesAllowed; +import org.aopalliance.intercept.MethodInvocation; import org.springframework.security.access.annotation.Secured; import org.springframework.security.access.prepost.PostAuthorize; import org.springframework.security.access.prepost.PostFilter; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreFilter; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.method.MethodAccessDeniedHandler; +import org.springframework.security.authorization.method.MethodInvocationResult; import org.springframework.security.core.Authentication; import org.springframework.security.core.parameters.P; @@ -108,4 +112,59 @@ public interface MethodSecurityService { @RequireAdminRole void repeatedAnnotations(); + @PostAuthorize(value = "hasRole('ADMIN')", handlerClass = CardNumberMaskingHandler.class) + String postAuthorizeGetCardNumberIfAdmin(String cardNumber); + + @PreAuthorize(value = "hasRole('ADMIN')", handlerClass = MaskingHandler.class) + String preAuthorizeGetCardNumberIfAdmin(String cardNumber); + + @PreAuthorize(value = "hasRole('ADMIN')", handlerClass = MaskingHandlerChild.class) + String preAuthorizeWithHandlerChildGetCardNumberIfAdmin(String cardNumber); + + @PreAuthorize(value = "hasRole('ADMIN')", handlerClass = MaskingHandler.class) + String preAuthorizeThrowAccessDeniedManually(); + + @PostAuthorize(value = "hasRole('ADMIN')", handlerClass = PostMaskingHandler.class) + String postAuthorizeThrowAccessDeniedManually(); + + class MaskingHandler implements MethodAccessDeniedHandler { + + @Override + public Object handle(MethodInvocation deniedObject, AuthorizationDecision decision) { + return "***"; + } + + } + + class MaskingHandlerChild extends MaskingHandler { + + @Override + public Object handle(MethodInvocation deniedObject, AuthorizationDecision decision) { + Object mask = super.handle(deniedObject, decision); + return mask + "-child"; + } + + } + + class PostMaskingHandler implements MethodAccessDeniedHandler { + + @Override + public Object handle(MethodInvocationResult deniedObject, AuthorizationDecision decision) { + return "***"; + } + + } + + class CardNumberMaskingHandler implements MethodAccessDeniedHandler { + + static String MASK = "****-****-****-"; + + @Override + public Object handle(MethodInvocationResult mi, AuthorizationDecision decision) { + String cardNumber = (String) mi.getResult(); + return MASK + cardNumber.substring(cardNumber.length() - 4); + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityServiceImpl.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityServiceImpl.java index ebe851d1f39..be51122526a 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityServiceImpl.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityServiceImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.util.List; +import org.springframework.security.access.AccessDeniedException; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; @@ -126,4 +127,29 @@ public List allAnnotations(List list) { public void repeatedAnnotations() { } + @Override + public String postAuthorizeGetCardNumberIfAdmin(String cardNumber) { + return cardNumber; + } + + @Override + public String preAuthorizeGetCardNumberIfAdmin(String cardNumber) { + return cardNumber; + } + + @Override + public String preAuthorizeWithHandlerChildGetCardNumberIfAdmin(String cardNumber) { + return cardNumber; + } + + @Override + public String preAuthorizeThrowAccessDeniedManually() { + throw new AccessDeniedException("Access Denied"); + } + + @Override + public String postAuthorizeThrowAccessDeniedManually() { + throw new AccessDeniedException("Access Denied"); + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java index f653d3c5875..ba33147730e 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java @@ -662,6 +662,66 @@ public void methodWhenPostFilterMetaAnnotationThenFilters() { .containsExactly("dave"); } + @Test + @WithMockUser + void getCardNumberWhenPostAuthorizeAndNotAdminThenReturnMasked() { + this.spring + .register(AuthzConfig.class, MethodSecurityServiceEnabledConfig.class, + MethodSecurityService.CardNumberMaskingHandler.class, MethodSecurityService.MaskingHandler.class) + .autowire(); + MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class); + String cardNumber = service.postAuthorizeGetCardNumberIfAdmin("4444-3333-2222-1111"); + assertThat(cardNumber).isEqualTo("****-****-****-1111"); + } + + @Test + @WithMockUser + void getCardNumberWhenPreAuthorizeAndNotAdminThenReturnMasked() { + this.spring + .register(AuthzConfig.class, MethodSecurityServiceEnabledConfig.class, + MethodSecurityService.MaskingHandler.class) + .autowire(); + MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class); + String cardNumber = service.preAuthorizeGetCardNumberIfAdmin("4444-3333-2222-1111"); + assertThat(cardNumber).isEqualTo("***"); + } + + @Test + @WithMockUser + void getCardNumberWhenPreAuthorizeAndNotAdminAndChildHandlerThenResolveCorrectHandlerAndReturnMasked() { + this.spring + .register(AuthzConfig.class, MethodSecurityServiceEnabledConfig.class, + MethodSecurityService.MaskingHandler.class, MethodSecurityService.MaskingHandlerChild.class) + .autowire(); + MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class); + String cardNumber = service.preAuthorizeWithHandlerChildGetCardNumberIfAdmin("4444-3333-2222-1111"); + assertThat(cardNumber).isEqualTo("***-child"); + } + + @Test + @WithMockUser(roles = "ADMIN") + void preAuthorizeWhenHandlerAndAccessDeniedNotThrownFromPreAuthorizeThenNotHandled() { + this.spring + .register(AuthzConfig.class, MethodSecurityServiceEnabledConfig.class, + MethodSecurityService.MaskingHandler.class) + .autowire(); + MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class); + assertThatExceptionOfType(AccessDeniedException.class) + .isThrownBy(service::preAuthorizeThrowAccessDeniedManually); + } + + @Test + @WithMockUser(roles = "ADMIN") + void postAuthorizeWhenHandlerAndAccessDeniedNotThrownFromPostAuthorizeThenNotHandled() { + this.spring + .register(AuthzConfig.class, MethodSecurityServiceEnabledConfig.class, + MethodSecurityService.PostMaskingHandler.class) + .autowire(); + MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class); + assertThatExceptionOfType(AccessDeniedException.class) + .isThrownBy(service::postAuthorizeThrowAccessDeniedManually); + } + private static Consumer disallowBeanOverriding() { return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false); } @@ -675,6 +735,16 @@ private static Advisor returnAdvisor(int order) { return advisor; } + @Configuration + static class AuthzConfig { + + @Bean + Authz authz() { + return new Authz(); + } + + } + @Configuration @EnableCustomMethodSecurity static class CustomMethodSecurityServiceConfig { diff --git a/core/src/main/java/org/springframework/security/access/prepost/PostAuthorize.java b/core/src/main/java/org/springframework/security/access/prepost/PostAuthorize.java index 18a60ef88f8..d79454c06a1 100644 --- a/core/src/main/java/org/springframework/security/access/prepost/PostAuthorize.java +++ b/core/src/main/java/org/springframework/security/access/prepost/PostAuthorize.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,10 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.security.authorization.method.DefaultPostInvocationMethodAccessDeniedHandler; +import org.springframework.security.authorization.method.MethodAccessDeniedHandler; +import org.springframework.security.authorization.method.MethodInvocationResult; + /** * Annotation for specifying a method access-control expression which will be evaluated * after a method has been invoked. @@ -42,4 +46,6 @@ */ String value(); + Class> handlerClass() default DefaultPostInvocationMethodAccessDeniedHandler.class; + } diff --git a/core/src/main/java/org/springframework/security/access/prepost/PreAuthorize.java b/core/src/main/java/org/springframework/security/access/prepost/PreAuthorize.java index ba711030533..81b4b71cee2 100644 --- a/core/src/main/java/org/springframework/security/access/prepost/PreAuthorize.java +++ b/core/src/main/java/org/springframework/security/access/prepost/PreAuthorize.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,11 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.security.authorization.method.DefaultPreInvocationMethodAccessDeniedHandler; +import org.springframework.security.authorization.method.MethodAccessDeniedHandler; + /** * Annotation for specifying a method access-control expression which will be evaluated to * decide whether a method invocation is allowed or not. @@ -42,4 +47,6 @@ */ String value(); + Class> handlerClass() default DefaultPreInvocationMethodAccessDeniedHandler.class; + } diff --git a/core/src/main/java/org/springframework/security/authorization/AuthorizationDecisionAccessDeniedException.java b/core/src/main/java/org/springframework/security/authorization/AuthorizationDecisionAccessDeniedException.java new file mode 100644 index 00000000000..b10438a2cac --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/AuthorizationDecisionAccessDeniedException.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authorization; + +import org.springframework.security.access.AccessDeniedException; +import org.springframework.util.Assert; + +public class AuthorizationDecisionAccessDeniedException extends AccessDeniedException { + + private final AuthorizationDecision decision; + + public AuthorizationDecisionAccessDeniedException(String msg, AuthorizationDecision decision) { + super(msg); + Assert.notNull(decision, "decision cannot be null"); + this.decision = decision; + } + + public AuthorizationDecision getDecision() { + return this.decision; + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptor.java index 4d490515f08..96221607e0e 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptor.java +++ b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.security.authorization.method; +import java.lang.reflect.Method; import java.util.function.Supplier; import org.aopalliance.aop.Advice; @@ -25,6 +26,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.aop.Pointcut; +import org.springframework.context.ApplicationContext; import org.springframework.core.log.LogMessage; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.prepost.PostAuthorize; @@ -59,6 +61,8 @@ public final class AuthorizationManagerAfterMethodInterceptor implements Authori private AuthorizationEventPublisher eventPublisher = AuthorizationManagerAfterMethodInterceptor::noPublish; + private MethodAccessDeniedHandlerResolver deniedHandlerResolver = new NullMethodAccessDeniedHandlerResolver(); + /** * Creates an instance. * @param pointcut the {@link Pointcut} to use @@ -116,8 +120,7 @@ public static AuthorizationManagerAfterMethodInterceptor postAuthorize( @Override public Object invoke(MethodInvocation mi) throws Throwable { Object result = mi.proceed(); - attemptAuthorization(mi, result); - return result; + return attemptAuthorization(mi, result); } @Override @@ -168,7 +171,12 @@ public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy strat this.securityContextHolderStrategy = () -> strategy; } - private void attemptAuthorization(MethodInvocation mi, Object result) { + public void setApplicationContext(ApplicationContext applicationContext) { + Assert.notNull(applicationContext, "applicationContext cannot be null"); + this.deniedHandlerResolver = new DefaultMethodAccessDeniedHandlerResolver(applicationContext); + } + + private Object attemptAuthorization(MethodInvocation mi, Object result) { this.logger.debug(LogMessage.of(() -> "Authorizing method invocation " + mi)); MethodInvocationResult object = new MethodInvocationResult(mi, result); AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, object); @@ -176,9 +184,19 @@ private void attemptAuthorization(MethodInvocation mi, Object result) { if (decision != null && !decision.isGranted()) { this.logger.debug(LogMessage.of(() -> "Failed to authorize " + mi + " with authorization manager " + this.authorizationManager + " and decision " + decision)); - throw new AccessDeniedException("Access Denied"); + return handleOrThrow(object, decision); } this.logger.debug(LogMessage.of(() -> "Authorized method invocation " + mi)); + return result; + } + + private Object handleOrThrow(MethodInvocationResult object, AuthorizationDecision decision) { + MethodAccessDeniedHandler handler = this.deniedHandlerResolver + .resolvePostInvocation(object.getMethodInvocation().getMethod()); + if (handler == null) { + throw new AccessDeniedException("Access Denied"); + } + return handler.handle(object, decision); } private Authentication getAuthentication() { @@ -195,4 +213,18 @@ private static void noPublish(Supplier authentication, T obj } + private static class NullMethodAccessDeniedHandlerResolver implements MethodAccessDeniedHandlerResolver { + + @Override + public MethodAccessDeniedHandler resolvePostInvocation(Method method) { + return null; + } + + @Override + public MethodAccessDeniedHandler resolvePreInvocation(Method method) { + return null; + } + + } + } diff --git a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.java index 4d84a55616d..151c1933319 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.java +++ b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.security.authorization.method; +import java.lang.reflect.Method; import java.util.function.Supplier; import jakarta.annotation.security.DenyAll; @@ -28,6 +29,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.aop.Pointcut; +import org.springframework.context.ApplicationContext; import org.springframework.core.log.LogMessage; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.annotation.Secured; @@ -63,6 +65,8 @@ public final class AuthorizationManagerBeforeMethodInterceptor implements Author private AuthorizationEventPublisher eventPublisher = AuthorizationManagerBeforeMethodInterceptor::noPublish; + private MethodAccessDeniedHandlerResolver deniedHandlerResolver = new NullMethodAccessDeniedHandlerResolver(); + /** * Creates an instance. * @param pointcut the {@link Pointcut} to use @@ -190,8 +194,7 @@ public static AuthorizationManagerBeforeMethodInterceptor jsr250( */ @Override public Object invoke(MethodInvocation mi) throws Throwable { - attemptAuthorization(mi); - return mi.proceed(); + return attemptAuthorization(mi); } @Override @@ -242,16 +245,31 @@ public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy secur this.securityContextHolderStrategy = () -> securityContextHolderStrategy; } - private void attemptAuthorization(MethodInvocation mi) { + public void setApplicationContext(ApplicationContext applicationContext) { + Assert.notNull(applicationContext, "applicationContext cannot be null"); + this.deniedHandlerResolver = new DefaultMethodAccessDeniedHandlerResolver(applicationContext); + } + + private Object attemptAuthorization(MethodInvocation mi) throws Throwable { this.logger.debug(LogMessage.of(() -> "Authorizing method invocation " + mi)); AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, mi); this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, mi, decision); if (decision != null && !decision.isGranted()) { this.logger.debug(LogMessage.of(() -> "Failed to authorize " + mi + " with authorization manager " + this.authorizationManager + " and decision " + decision)); - throw new AccessDeniedException("Access Denied"); + return handleOrThrow(mi, decision); } this.logger.debug(LogMessage.of(() -> "Authorized method invocation " + mi)); + return mi.proceed(); + } + + private Object handleOrThrow(MethodInvocation mi, AuthorizationDecision decision) { + MethodAccessDeniedHandler handler = this.deniedHandlerResolver + .resolvePreInvocation(mi.getMethod()); + if (handler == null) { + throw new AccessDeniedException("Access Denied"); + } + return handler.handle(mi, decision); } private Authentication getAuthentication() { @@ -268,4 +286,18 @@ private static void noPublish(Supplier authentication, T obj } + private static class NullMethodAccessDeniedHandlerResolver implements MethodAccessDeniedHandlerResolver { + + @Override + public MethodAccessDeniedHandler resolvePostInvocation(Method method) { + return null; + } + + @Override + public MethodAccessDeniedHandler resolvePreInvocation(Method method) { + return null; + } + + } + } diff --git a/core/src/main/java/org/springframework/security/authorization/method/DefaultMethodAccessDeniedHandlerResolver.java b/core/src/main/java/org/springframework/security/authorization/method/DefaultMethodAccessDeniedHandlerResolver.java new file mode 100644 index 00000000000..b282158eb9f --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/method/DefaultMethodAccessDeniedHandlerResolver.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authorization.method; + +import java.lang.reflect.Method; +import java.util.Arrays; + +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.context.ApplicationContext; +import org.springframework.security.access.prepost.PostAuthorize; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.util.Assert; + +final class DefaultMethodAccessDeniedHandlerResolver implements MethodAccessDeniedHandlerResolver { + + private final ApplicationContext context; + + DefaultMethodAccessDeniedHandlerResolver(ApplicationContext applicationContext) { + Assert.notNull(applicationContext, "applicationContext cannot be null"); + this.context = applicationContext; + } + + @Override + public MethodAccessDeniedHandler resolvePostInvocation(Method method) { + PostAuthorize postAuthorize = AuthorizationAnnotationUtils.findUniqueAnnotation(method, PostAuthorize.class); + if (postAuthorize == null) { + return null; + } + Class> handlerClass = postAuthorize.handlerClass(); + return getBean(handlerClass); + } + + @Override + public MethodAccessDeniedHandler resolvePreInvocation(Method method) { + PreAuthorize preAuthorize = AuthorizationAnnotationUtils.findUniqueAnnotation(method, PreAuthorize.class); + if (preAuthorize == null) { + return null; + } + Class> handlerClass = preAuthorize.handlerClass(); + return getBean(handlerClass); + } + + private MethodAccessDeniedHandler getBean(Class> beanClass) { + String[] beanNames = this.context.getBeanNamesForType(beanClass); + if (beanNames.length == 0) { + throw new IllegalStateException("Could not find a bean of type " + beanClass.getSimpleName()); + } + if (beanNames.length > 1) { + throw new IllegalStateException("Expected to find a single bean of type " + beanClass.getSimpleName() + + " but found " + Arrays.toString(beanNames)); + } + return this.context.getBean(beanNames[0], beanClass); + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/method/DefaultPostInvocationMethodAccessDeniedHandler.java b/core/src/main/java/org/springframework/security/authorization/method/DefaultPostInvocationMethodAccessDeniedHandler.java new file mode 100644 index 00000000000..9bb7600aa2e --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/method/DefaultPostInvocationMethodAccessDeniedHandler.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authorization.method; + +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationDecisionAccessDeniedException; + +public class DefaultPostInvocationMethodAccessDeniedHandler + implements MethodAccessDeniedHandler { + + @Override + public Object handle(MethodInvocationResult deniedObject, AuthorizationDecision decision) { + throw new AuthorizationDecisionAccessDeniedException("Access Denied", decision); + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/method/DefaultPreInvocationMethodAccessDeniedHandler.java b/core/src/main/java/org/springframework/security/authorization/method/DefaultPreInvocationMethodAccessDeniedHandler.java new file mode 100644 index 00000000000..03dce5d87fb --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/method/DefaultPreInvocationMethodAccessDeniedHandler.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authorization.method; + +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationDecisionAccessDeniedException; + +public class DefaultPreInvocationMethodAccessDeniedHandler implements MethodAccessDeniedHandler { + + @Override + public Object handle(MethodInvocation deniedObject, AuthorizationDecision decision) { + throw new AuthorizationDecisionAccessDeniedException("Access Denied", decision); + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/method/MethodAccessDeniedHandler.java b/core/src/main/java/org/springframework/security/authorization/method/MethodAccessDeniedHandler.java new file mode 100644 index 00000000000..3e77d41643d --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/method/MethodAccessDeniedHandler.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authorization.method; + +import org.springframework.lang.Nullable; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authorization.AuthorizationDecision; + +public interface MethodAccessDeniedHandler { + + @Nullable + Object handle(T deniedObject, AuthorizationDecision decision); + +} diff --git a/core/src/main/java/org/springframework/security/authorization/method/MethodAccessDeniedHandlerResolver.java b/core/src/main/java/org/springframework/security/authorization/method/MethodAccessDeniedHandlerResolver.java new file mode 100644 index 00000000000..b71fc9c98e9 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/method/MethodAccessDeniedHandlerResolver.java @@ -0,0 +1,29 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authorization.method; + +import java.lang.reflect.Method; + +import org.aopalliance.intercept.MethodInvocation; + +interface MethodAccessDeniedHandlerResolver { + + MethodAccessDeniedHandler resolvePostInvocation(Method method); + + MethodAccessDeniedHandler resolvePreInvocation(Method method); + +}