From d51006d7c8e4a985092cbaf4fb08ccf01b84481f Mon Sep 17 00:00:00 2001 From: Marcus Hert Da Coregio Date: Fri, 8 Mar 2024 10:46:27 -0300 Subject: [PATCH] Allow post-processing of authorization denied results with @PreAuthorize and @PostAuthorize --- ...thorizationPostProcessorConfiguration.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 ++- .../authorization/AuthorizationException.java | 37 ++++++++++ .../AuthorizationDeniedPostProcessor.java | 27 +++++++ ...rizationManagerAfterMethodInterceptor.java | 60 ++++++++++++++-- ...izationManagerBeforeMethodInterceptor.java | 59 ++++++++++++++-- ...ationAuthorizationDeniedPostProcessor.java | 30 ++++++++ ...ationAuthorizationDeniedPostProcessor.java | 32 +++++++++ .../MethodAccessDeniedHandlerResolver.java | 29 ++++++++ .../PostAuthorizeAuthorizationManager.java | 9 +-- .../PostAuthorizeExpressionAttribute.java | 37 ++++++++++ ...tAuthorizeExpressionAttributeRegistry.java | 2 +- .../PostProcessableAuthorizationDecision.java | 54 ++++++++++++++ .../PreAuthorizeAuthorizationManager.java | 7 +- .../PreAuthorizeExpressionAttribute.java | 39 +++++++++++ ...eAuthorizeExpressionAttributeRegistry.java | 2 +- 22 files changed, 617 insertions(+), 23 deletions(-) create mode 100644 config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationPostProcessorConfiguration.java create mode 100644 core/src/main/java/org/springframework/security/authorization/AuthorizationException.java create mode 100644 core/src/main/java/org/springframework/security/authorization/method/AuthorizationDeniedPostProcessor.java create mode 100644 core/src/main/java/org/springframework/security/authorization/method/DefaultPostInvocationAuthorizationDeniedPostProcessor.java create mode 100644 core/src/main/java/org/springframework/security/authorization/method/DefaultPreInvocationAuthorizationDeniedPostProcessor.java create mode 100644 core/src/main/java/org/springframework/security/authorization/method/MethodAccessDeniedHandlerResolver.java create mode 100644 core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeExpressionAttribute.java create mode 100644 core/src/main/java/org/springframework/security/authorization/method/PostProcessableAuthorizationDecision.java create mode 100644 core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeExpressionAttribute.java diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationPostProcessorConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationPostProcessorConfiguration.java new file mode 100644 index 00000000000..37269ddd80c --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationPostProcessorConfiguration.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.DefaultPostInvocationAuthorizationDeniedPostProcessor; +import org.springframework.security.authorization.method.DefaultPreInvocationAuthorizationDeniedPostProcessor; + +@Configuration(proxyBeanMethods = false) +class AuthorizationPostProcessorConfiguration { + + @Bean + DefaultPreInvocationAuthorizationDeniedPostProcessor defaultPreAuthorizeMethodAccessDeniedHandler() { + return new DefaultPreInvocationAuthorizationDeniedPostProcessor(); + } + + @Bean + DefaultPostInvocationAuthorizationDeniedPostProcessor defaultPostAuthorizeMethodAccessDeniedHandler() { + return new DefaultPostInvocationAuthorizationDeniedPostProcessor(); + } + +} 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..15b96622487 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(AuthorizationPostProcessorConfiguration.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..6d0b1672c72 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.AuthorizationResult; +import org.springframework.security.authorization.method.AuthorizationDeniedPostProcessor; +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')", postProcessorClass = CardNumberMaskingPostProcessor.class) + String postAuthorizeGetCardNumberIfAdmin(String cardNumber); + + @PreAuthorize(value = "hasRole('ADMIN')", postProcessorClass = MaskingPostProcessor.class) + String preAuthorizeGetCardNumberIfAdmin(String cardNumber); + + @PreAuthorize(value = "hasRole('ADMIN')", postProcessorClass = MaskingPostProcessorChild.class) + String preAuthorizeWithHandlerChildGetCardNumberIfAdmin(String cardNumber); + + @PreAuthorize(value = "hasRole('ADMIN')", postProcessorClass = MaskingPostProcessor.class) + String preAuthorizeThrowAccessDeniedManually(); + + @PostAuthorize(value = "hasRole('ADMIN')", postProcessorClass = PostMaskingPostProcessor.class) + String postAuthorizeThrowAccessDeniedManually(); + + class MaskingPostProcessor implements AuthorizationDeniedPostProcessor { + + @Override + public Object postProcessResult(MethodInvocation contextObject, AuthorizationResult result) { + return "***"; + } + + } + + class MaskingPostProcessorChild extends MaskingPostProcessor { + + @Override + public Object postProcessResult(MethodInvocation contextObject, AuthorizationResult result) { + Object mask = super.postProcessResult(contextObject, result); + return mask + "-child"; + } + + } + + class PostMaskingPostProcessor implements AuthorizationDeniedPostProcessor { + + @Override + public Object postProcessResult(MethodInvocationResult contextObject, AuthorizationResult result) { + return "***"; + } + + } + + class CardNumberMaskingPostProcessor implements AuthorizationDeniedPostProcessor { + + static String MASK = "****-****-****-"; + + @Override + public Object postProcessResult(MethodInvocationResult contextObject, AuthorizationResult result) { + String cardNumber = (String) contextObject.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..356cc87c134 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.CardNumberMaskingPostProcessor.class, + MethodSecurityService.MaskingPostProcessor.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.MaskingPostProcessor.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.MaskingPostProcessor.class, MethodSecurityService.MaskingPostProcessorChild.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.MaskingPostProcessor.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.PostMaskingPostProcessor.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..2968a3c11e7 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.AuthorizationDeniedPostProcessor; +import org.springframework.security.authorization.method.DefaultPostInvocationAuthorizationDeniedPostProcessor; +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> postProcessorClass() default DefaultPostInvocationAuthorizationDeniedPostProcessor.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..806c7a6a980 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.AuthorizationDeniedPostProcessor; +import org.springframework.security.authorization.method.DefaultPreInvocationAuthorizationDeniedPostProcessor; + /** * 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> postProcessorClass() default DefaultPreInvocationAuthorizationDeniedPostProcessor.class; + } diff --git a/core/src/main/java/org/springframework/security/authorization/AuthorizationException.java b/core/src/main/java/org/springframework/security/authorization/AuthorizationException.java new file mode 100644 index 00000000000..4bf6ee07b26 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/AuthorizationException.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.authorization; + +import org.springframework.security.access.AccessDeniedException; +import org.springframework.util.Assert; + +public class AuthorizationException extends AccessDeniedException { + + private final AuthorizationResult result; + + public AuthorizationException(String msg, AuthorizationResult result) { + super(msg); + Assert.notNull(result, "decision cannot be null"); + Assert.state(!result.isGranted(), "Granted decisions are not supported"); + this.result = result; + } + + public AuthorizationResult getResult() { + return this.result; + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationDeniedPostProcessor.java b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationDeniedPostProcessor.java new file mode 100644 index 00000000000..fa1f17c011b --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationDeniedPostProcessor.java @@ -0,0 +1,27 @@ +/* + * 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.authorization.AuthorizationResult; + +public interface AuthorizationDeniedPostProcessor { + + @Nullable + Object postProcessResult(T contextObject, AuthorizationResult result); + +} 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..bd0266bbdc7 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.util.Arrays; import java.util.function.Supplier; import org.aopalliance.aop.Advice; @@ -25,6 +26,8 @@ import org.apache.commons.logging.LogFactory; import org.springframework.aop.Pointcut; +import org.springframework.context.ApplicationContext; +import org.springframework.core.ResolvableType; import org.springframework.core.log.LogMessage; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.prepost.PostAuthorize; @@ -55,10 +58,14 @@ public final class AuthorizationManagerAfterMethodInterceptor implements Authori private final AuthorizationManager authorizationManager; + private final AuthorizationDeniedPostProcessor defaultPostProcessor = new DefaultPostInvocationAuthorizationDeniedPostProcessor(); + private int order; private AuthorizationEventPublisher eventPublisher = AuthorizationManagerAfterMethodInterceptor::noPublish; + private ApplicationContext context; + /** * Creates an instance. * @param pointcut the {@link Pointcut} to use @@ -116,8 +123,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 +174,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.context = 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 +187,48 @@ 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 postProcess(object, decision); } this.logger.debug(LogMessage.of(() -> "Authorized method invocation " + mi)); + return result; + } + + private Object postProcess(MethodInvocationResult mi, AuthorizationDecision decision) { + Class> postProcessorClass = getPostProcessorClass( + decision); + AuthorizationDeniedPostProcessor postProcessor = resolvePostProcessor( + postProcessorClass); + return postProcessor.postProcessResult(mi, decision); + } + + @SuppressWarnings({ "unchecked", "DataFlowIssue" }) + private Class> getPostProcessorClass( + AuthorizationDecision decision) { + if (!(decision instanceof PostProcessableAuthorizationDecision postProcessableDecision)) { + return null; + } + if (!MethodInvocationResult.class + .isAssignableFrom(ResolvableType.forInstance(postProcessableDecision).resolveGeneric(0))) { + return null; + } + return (Class>) postProcessableDecision + .getPostProcessorClass(); + } + + private AuthorizationDeniedPostProcessor resolvePostProcessor( + Class> beanClass) { + if (beanClass == null || this.context == null) { + return this.defaultPostProcessor; + } + 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); } private Authentication getAuthentication() { 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..afba3395cc3 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.util.Arrays; import java.util.function.Supplier; import jakarta.annotation.security.DenyAll; @@ -28,6 +29,8 @@ import org.apache.commons.logging.LogFactory; import org.springframework.aop.Pointcut; +import org.springframework.context.ApplicationContext; +import org.springframework.core.ResolvableType; import org.springframework.core.log.LogMessage; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.annotation.Secured; @@ -59,10 +62,14 @@ public final class AuthorizationManagerBeforeMethodInterceptor implements Author private final AuthorizationManager authorizationManager; + private final AuthorizationDeniedPostProcessor defaultPostProcessor = new DefaultPreInvocationAuthorizationDeniedPostProcessor(); + private int order = AuthorizationInterceptorsOrder.FIRST.getOrder(); private AuthorizationEventPublisher eventPublisher = AuthorizationManagerBeforeMethodInterceptor::noPublish; + private ApplicationContext context; + /** * Creates an instance. * @param pointcut the {@link Pointcut} to use @@ -190,8 +197,7 @@ public static AuthorizationManagerBeforeMethodInterceptor jsr250( */ @Override public Object invoke(MethodInvocation mi) throws Throwable { - attemptAuthorization(mi); - return mi.proceed(); + return attemptAuthorization(mi); } @Override @@ -242,16 +248,59 @@ 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.context = 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 postProcess(mi, decision); } this.logger.debug(LogMessage.of(() -> "Authorized method invocation " + mi)); + return mi.proceed(); + } + + private Object postProcess(MethodInvocation mi, AuthorizationDecision decision) { + Class> postProcessorClass = getPostProcessorClass( + decision); + AuthorizationDeniedPostProcessor postProcessor = resolvePostProcessor(postProcessorClass); + return postProcessor.postProcessResult(mi, decision); + } + + @SuppressWarnings({ "unchecked", "DataFlowIssue" }) + private Class> getPostProcessorClass( + AuthorizationDecision decision) { + if (!(decision instanceof PostProcessableAuthorizationDecision postProcessableDecision)) { + return null; + } + Class genericType = ResolvableType.forInstance(postProcessableDecision).resolveGeneric(0); + if (!MethodInvocation.class.isAssignableFrom(genericType)) { + return null; + } + return (Class>) postProcessableDecision + .getPostProcessorClass(); + } + + private AuthorizationDeniedPostProcessor resolvePostProcessor( + Class> beanClass) { + if (beanClass == null || this.context == null) { + return this.defaultPostProcessor; + } + 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); } private Authentication getAuthentication() { diff --git a/core/src/main/java/org/springframework/security/authorization/method/DefaultPostInvocationAuthorizationDeniedPostProcessor.java b/core/src/main/java/org/springframework/security/authorization/method/DefaultPostInvocationAuthorizationDeniedPostProcessor.java new file mode 100644 index 00000000000..782556bc045 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/method/DefaultPostInvocationAuthorizationDeniedPostProcessor.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.AuthorizationException; +import org.springframework.security.authorization.AuthorizationResult; + +public class DefaultPostInvocationAuthorizationDeniedPostProcessor + implements AuthorizationDeniedPostProcessor { + + @Override + public Object postProcessResult(MethodInvocationResult contextObject, AuthorizationResult result) { + throw new AuthorizationException("Access Denied", result); + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/method/DefaultPreInvocationAuthorizationDeniedPostProcessor.java b/core/src/main/java/org/springframework/security/authorization/method/DefaultPreInvocationAuthorizationDeniedPostProcessor.java new file mode 100644 index 00000000000..5aa3fd60c79 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/method/DefaultPreInvocationAuthorizationDeniedPostProcessor.java @@ -0,0 +1,32 @@ +/* + * 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.AuthorizationException; +import org.springframework.security.authorization.AuthorizationResult; + +public class DefaultPreInvocationAuthorizationDeniedPostProcessor + implements AuthorizationDeniedPostProcessor { + + @Override + public Object postProcessResult(MethodInvocation contextObject, AuthorizationResult result) { + throw new AuthorizationException("Access Denied", result); + } + +} 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..74528406cb7 --- /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 { + + AuthorizationDeniedPostProcessor resolvePostInvocation(Method method); + + AuthorizationDeniedPostProcessor resolvePreInvocation(Method method); + +} diff --git a/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.java index ee8e26d58a8..177c1150f47 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 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. @@ -26,7 +26,6 @@ import org.springframework.security.access.prepost.PostAuthorize; import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationManager; -import org.springframework.security.authorization.ExpressionAuthorizationDecision; import org.springframework.security.core.Authentication; /** @@ -76,11 +75,13 @@ public AuthorizationDecision check(Supplier authentication, Meth if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) { return null; } + PostAuthorizeExpressionAttribute postAuthorizeAttribute = (PostAuthorizeExpressionAttribute) attribute; MethodSecurityExpressionHandler expressionHandler = this.registry.getExpressionHandler(); EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication, mi.getMethodInvocation()); expressionHandler.setReturnObject(mi.getResult(), ctx); - boolean granted = ExpressionUtils.evaluateAsBoolean(attribute.getExpression(), ctx); - return new ExpressionAuthorizationDecision(granted, attribute.getExpression()); + boolean granted = ExpressionUtils.evaluateAsBoolean(postAuthorizeAttribute.getExpression(), ctx); + return new PostProcessableAuthorizationDecision<>(granted, postAuthorizeAttribute.getExpression(), mi, + postAuthorizeAttribute.getPostProcessorClass()); } } diff --git a/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeExpressionAttribute.java b/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeExpressionAttribute.java new file mode 100644 index 00000000000..17d63b93ba9 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeExpressionAttribute.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.authorization.method; + +import org.springframework.expression.Expression; +import org.springframework.util.Assert; + +class PostAuthorizeExpressionAttribute extends ExpressionAttribute { + + private final Class> postProcessorClass; + + PostAuthorizeExpressionAttribute(Expression expression, + Class> postProcessorClass) { + super(expression); + Assert.notNull(postProcessorClass, "postProcessorClass cannot be null"); + this.postProcessorClass = postProcessorClass; + } + + Class> getPostProcessorClass() { + return this.postProcessorClass; + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeExpressionAttributeRegistry.java b/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeExpressionAttributeRegistry.java index a7ca347b4aa..d3534393b98 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeExpressionAttributeRegistry.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeExpressionAttributeRegistry.java @@ -44,7 +44,7 @@ ExpressionAttribute resolveAttribute(Method method, Class targetClass) { return ExpressionAttribute.NULL_ATTRIBUTE; } Expression expression = getExpressionHandler().getExpressionParser().parseExpression(postAuthorize.value()); - return new ExpressionAttribute(expression); + return new PostAuthorizeExpressionAttribute(expression, postAuthorize.postProcessorClass()); } private PostAuthorize findPostAuthorizeAnnotation(Method method, Class targetClass) { diff --git a/core/src/main/java/org/springframework/security/authorization/method/PostProcessableAuthorizationDecision.java b/core/src/main/java/org/springframework/security/authorization/method/PostProcessableAuthorizationDecision.java new file mode 100644 index 00000000000..3adef1070d3 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/method/PostProcessableAuthorizationDecision.java @@ -0,0 +1,54 @@ +/* + * 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.core.ResolvableType; +import org.springframework.core.ResolvableTypeProvider; +import org.springframework.expression.Expression; +import org.springframework.security.authorization.ExpressionAuthorizationDecision; +import org.springframework.util.Assert; + +class PostProcessableAuthorizationDecision extends ExpressionAuthorizationDecision + implements ResolvableTypeProvider { + + private final T contextObject; + + private final Class> postProcessorClass; + + PostProcessableAuthorizationDecision(boolean granted, Expression expressionAttribute, T contextObject, + Class> postProcessorClass) { + super(granted, expressionAttribute); + Assert.notNull(contextObject, "contextObject cannot be null"); + Assert.notNull(postProcessorClass, "postProcessorClass cannot be null"); + this.contextObject = contextObject; + this.postProcessorClass = postProcessorClass; + } + + T getContextObject() { + return this.contextObject; + } + + Class> getPostProcessorClass() { + return this.postProcessorClass; + } + + @Override + public ResolvableType getResolvableType() { + return ResolvableType.forClassWithGenerics(getClass(), this.contextObject.getClass()); + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.java index c1f7a6b9e02..fb58c259f8b 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.java @@ -26,7 +26,6 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationManager; -import org.springframework.security.authorization.ExpressionAuthorizationDecision; import org.springframework.security.core.Authentication; /** @@ -76,9 +75,11 @@ public AuthorizationDecision check(Supplier authentication, Meth if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) { return null; } + PreAuthorizeExpressionAttribute preAuthorizeAttribute = (PreAuthorizeExpressionAttribute) attribute; EvaluationContext ctx = this.registry.getExpressionHandler().createEvaluationContext(authentication, mi); - boolean granted = ExpressionUtils.evaluateAsBoolean(attribute.getExpression(), ctx); - return new ExpressionAuthorizationDecision(granted, attribute.getExpression()); + boolean granted = ExpressionUtils.evaluateAsBoolean(preAuthorizeAttribute.getExpression(), ctx); + return new PostProcessableAuthorizationDecision<>(granted, preAuthorizeAttribute.getExpression(), mi, + preAuthorizeAttribute.getPostProcessorClass()); } } diff --git a/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeExpressionAttribute.java b/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeExpressionAttribute.java new file mode 100644 index 00000000000..0a33d4fc61f --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeExpressionAttribute.java @@ -0,0 +1,39 @@ +/* + * 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.expression.Expression; +import org.springframework.util.Assert; + +class PreAuthorizeExpressionAttribute extends ExpressionAttribute { + + private final Class> postProcessorClass; + + PreAuthorizeExpressionAttribute(Expression expression, + Class> postProcessorClass) { + super(expression); + Assert.notNull(postProcessorClass, "postProcessorClass cannot be null"); + this.postProcessorClass = postProcessorClass; + } + + Class> getPostProcessorClass() { + return this.postProcessorClass; + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeExpressionAttributeRegistry.java b/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeExpressionAttributeRegistry.java index 38994412980..52b743c82c6 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeExpressionAttributeRegistry.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeExpressionAttributeRegistry.java @@ -44,7 +44,7 @@ ExpressionAttribute resolveAttribute(Method method, Class targetClass) { return ExpressionAttribute.NULL_ATTRIBUTE; } Expression expression = getExpressionHandler().getExpressionParser().parseExpression(preAuthorize.value()); - return new ExpressionAttribute(expression); + return new PreAuthorizeExpressionAttribute(expression, preAuthorize.postProcessorClass()); } private PreAuthorize findPreAuthorizeAnnotation(Method method, Class targetClass) {