diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyConfiguration.java index 1a126bfe5e3..6d3d0f2ce3c 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyConfiguration.java @@ -25,6 +25,7 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.DependsOn; import org.springframework.context.annotation.Role; import org.springframework.security.aot.hint.AuthorizeReturnObjectHintsRegistrar; import org.springframework.security.aot.hint.SecurityHintsRegistrar; @@ -32,6 +33,7 @@ import org.springframework.security.authorization.method.AuthorizationAdvisor; import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory; import org.springframework.security.authorization.method.AuthorizeReturnObjectMethodInterceptor; +import org.springframework.security.authorization.method.PermitParameterMethodInterceptor; import org.springframework.security.config.Customizer; @Configuration(proxyBeanMethods = false) @@ -48,6 +50,13 @@ static AuthorizationAdvisorProxyFactory authorizationProxyFactory( @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static MethodInterceptor permitParameterMethodInterceptor() { + return new PermitParameterMethodInterceptor(); + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + @DependsOn("permitParameterMethodInterceptor") static MethodInterceptor authorizeReturnObjectMethodInterceptor(ObjectProvider provider, AuthorizationAdvisorProxyFactory authorizationProxyFactory) { provider.forEach(authorizationProxyFactory::addAdvisor); 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 4fa7b8a16e0..5cfe4103efc 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 @@ -981,7 +981,7 @@ void autowireWhenDefaultsThenCreatesExactlyOneAdvisorPerAnnotation() { assertThat(this.spring.getContext().getBeanNamesForType(AuthorizationAdvisor.class)).hasSize(5) .containsExactlyInAnyOrder("preFilterAuthorizationMethodInterceptor", "preAuthorizeAuthorizationMethodInterceptor", "postAuthorizeAuthorizationMethodInterceptor", - "postFilterAuthorizationMethodInterceptor", "authorizeReturnObjectMethodInterceptor"); + "postFilterAuthorizationMethodInterceptor", "authorizeReturnObjectMethodInterceptor", "permitParameterMethodInterceptor"); } // gh-15592 @@ -994,7 +994,7 @@ void autowireWhenAspectJAutoProxyAndFactoryBeanThenExactlyOneAdvisorPerAnnotatio assertThat(this.spring.getContext().getBeanNamesForType(AuthorizationAdvisor.class)).hasSize(5) .containsExactlyInAnyOrder("preFilterAuthorizationMethodInterceptor", "preAuthorizeAuthorizationMethodInterceptor", "postAuthorizeAuthorizationMethodInterceptor", - "postFilterAuthorizationMethodInterceptor", "authorizeReturnObjectMethodInterceptor"); + "postFilterAuthorizationMethodInterceptor", "authorizeReturnObjectMethodInterceptor", "permitParameterMethodInterceptor"); } // gh-15651 diff --git a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationInterceptorsOrder.java b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationInterceptorsOrder.java index c92d39db185..d139d805e27 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationInterceptorsOrder.java +++ b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationInterceptorsOrder.java @@ -41,7 +41,9 @@ public enum AuthorizationInterceptorsOrder { SECURED, - JSR250, + JSR250(400), + + PERMIT_PARAMETER(425), SECURE_RESULT(450), diff --git a/core/src/main/java/org/springframework/security/authorization/method/PermitParameter.java b/core/src/main/java/org/springframework/security/authorization/method/PermitParameter.java new file mode 100644 index 00000000000..c9ceb46ecfe --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/method/PermitParameter.java @@ -0,0 +1,41 @@ +/* + * 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.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Unwraps Spring Security method authorization advice from method parameters of any + * method this annotation is applied to. + * + *

+ * Placing this at the class level is semantically identical to placing it on each method + * in that class. + *

+ * + * @author Josh Cummings + * @since 6.4 + * @see PermitParameterMethodInterceptor + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +public @interface PermitParameter { + +} diff --git a/core/src/main/java/org/springframework/security/authorization/method/PermitParameterMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/PermitParameterMethodInterceptor.java new file mode 100644 index 00000000000..856d9e8cc4c --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/method/PermitParameterMethodInterceptor.java @@ -0,0 +1,92 @@ +/* + * 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.lang.reflect.Parameter; + +import org.aopalliance.aop.Advice; +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.aop.Pointcut; +import org.springframework.aop.support.StaticMethodMatcherPointcut; +import org.springframework.security.core.annotation.SecurityAnnotationScanner; +import org.springframework.security.core.annotation.SecurityAnnotationScanners; + +public final class PermitParameterMethodInterceptor implements AuthorizationAdvisor { + + private final SecurityAnnotationScanner scanner = SecurityAnnotationScanners + .requireUnique(PermitParameter.class); + + private Pointcut pointcut = new StaticMethodMatcherPointcut() { + @Override + public boolean matches(Method method, Class targetClass) { + return PermitParameterMethodInterceptor.this.scanner.scan(method, targetClass) != null + || anyParameters(method); + } + + private boolean anyParameters(Method method) { + for (Parameter parameter : method.getParameters()) { + if (PermitParameterMethodInterceptor.this.scanner.scan(parameter) != null) { + return true; + } + } + return false; + } + }; + + private int order = AuthorizationInterceptorsOrder.PERMIT_PARAMETER.getOrder(); + + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { + Object[] args = invocation.getArguments(); + boolean entireMethod = this.scanner.scan(invocation.getMethod(), invocation.getThis().getClass()) != null; + for (int i = 0; i < args.length; i++) { + if (!entireMethod && this.scanner.scan(invocation.getMethod().getParameters()[i]) == null) { + continue; + } + if (args[i] instanceof AuthorizationProxy authorized) { + args[i] = authorized.toAuthorizedTarget(); + } + } + return invocation.proceed(); + } + + @Override + public Pointcut getPointcut() { + return this.pointcut; + } + + public void setPointcut(Pointcut pointcut) { + this.pointcut = pointcut; + } + + @Override + public Advice getAdvice() { + return this; + } + + @Override + public int getOrder() { + return this.order; + } + + public void setOrder(int order) { + this.order = order; + } + +}