diff --git a/config/spring-security-config.gradle b/config/spring-security-config.gradle
index 8e79db8d5f5..19100561657 100644
--- a/config/spring-security-config.gradle
+++ b/config/spring-security-config.gradle
@@ -37,6 +37,7 @@ dependencies {
optional'org.springframework:spring-websocket'
optional 'org.jetbrains.kotlin:kotlin-reflect'
optional 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
+ optional 'javax.annotation:jsr250-api'
provided 'javax.servlet:javax.servlet-api'
diff --git a/config/src/main/java/org/springframework/security/config/Elements.java b/config/src/main/java/org/springframework/security/config/Elements.java
index 20ad6157692..55e0dbaa30f 100644
--- a/config/src/main/java/org/springframework/security/config/Elements.java
+++ b/config/src/main/java/org/springframework/security/config/Elements.java
@@ -87,6 +87,8 @@ public abstract class Elements {
public static final String GLOBAL_METHOD_SECURITY = "global-method-security";
+ public static final String METHOD_SECURITY = "method-security";
+
public static final String PASSWORD_ENCODER = "password-encoder";
public static final String PORT_MAPPINGS = "port-mappings";
diff --git a/config/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java b/config/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java
index 9a110868709..0f23a77f707 100644
--- a/config/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java
+++ b/config/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java
@@ -44,6 +44,7 @@
import org.springframework.security.config.ldap.LdapUserServiceBeanDefinitionParser;
import org.springframework.security.config.method.GlobalMethodSecurityBeanDefinitionParser;
import org.springframework.security.config.method.InterceptMethodsBeanDefinitionDecorator;
+import org.springframework.security.config.method.MethodSecurityBeanDefinitionParser;
import org.springframework.security.config.method.MethodSecurityMetadataSourceBeanDefinitionParser;
import org.springframework.security.config.oauth2.client.ClientRegistrationsBeanDefinitionParser;
import org.springframework.security.config.websocket.WebSocketMessageBrokerSecurityBeanDefinitionParser;
@@ -169,6 +170,7 @@ private void loadParsers() {
this.parsers.put(Elements.JDBC_USER_SERVICE, new JdbcUserServiceBeanDefinitionParser());
this.parsers.put(Elements.AUTHENTICATION_PROVIDER, new AuthenticationProviderBeanDefinitionParser());
this.parsers.put(Elements.GLOBAL_METHOD_SECURITY, new GlobalMethodSecurityBeanDefinitionParser());
+ this.parsers.put(Elements.METHOD_SECURITY, new MethodSecurityBeanDefinitionParser());
this.parsers.put(Elements.AUTHENTICATION_MANAGER, new AuthenticationManagerBeanDefinitionParser());
this.parsers.put(Elements.METHOD_SECURITY_METADATA_SOURCE,
new MethodSecurityMetadataSourceBeanDefinitionParser());
diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/EnableMethodSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/EnableMethodSecurity.java
new file mode 100644
index 00000000000..1ddae277018
--- /dev/null
+++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/EnableMethodSecurity.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2002-2021 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 java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.springframework.context.annotation.AdviceMode;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+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;
+
+/**
+ * Enables Spring Security Method Security.
+ * @author Evgeniy Cheban
+ * @author Josh Cummings
+ * @since 5.6
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+@Documented
+@Import(MethodSecuritySelector.class)
+@Configuration
+public @interface EnableMethodSecurity {
+
+ /**
+ * Determines if Spring Security's {@link PreAuthorize}, {@link PostAuthorize},
+ * {@link PreFilter}, and {@link PostFilter} annotations should be enabled. Default is
+ * true.
+ * @return true if pre/post annotations should be enabled false otherwise
+ */
+ boolean prePostEnabled() default true;
+
+ /**
+ * Determines if Spring Security's {@link Secured} annotation should be enabled.
+ * Default is false.
+ * @return true if {@link Secured} annotation should be enabled false otherwise
+ */
+ boolean securedEnabled() default false;
+
+ /**
+ * Determines if JSR-250 annotations should be enabled. Default is false.
+ * @return true if JSR-250 should be enabled false otherwise
+ */
+ boolean jsr250Enabled() default false;
+
+ /**
+ * Indicate whether subclass-based (CGLIB) proxies are to be created as opposed to
+ * standard Java interface-based proxies. The default is {@code false}.
+ * Applicable only if {@link #mode()} is set to {@link AdviceMode#PROXY} .
+ *
+ * Note that setting this attribute to {@code true} will affect all
+ * Spring-managed beans requiring proxying, not just those marked with
+ * {@code @Cacheable}. For example, other beans marked with Spring's
+ * {@code @Transactional} annotation will be upgraded to subclass proxying at the same
+ * time. This approach has no negative impact in practice unless one is explicitly
+ * expecting one type of proxy vs another, e.g. in tests.
+ * @return true if subclass-based (CGLIB) proxies are to be created
+ */
+ boolean proxyTargetClass() default false;
+
+ /**
+ * Indicate how security advice should be applied. The default is
+ * {@link AdviceMode#PROXY}.
+ * @see AdviceMode
+ * @return the {@link AdviceMode} to use
+ */
+ AdviceMode mode() default AdviceMode.PROXY;
+
+}
diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/Jsr250MethodSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/Jsr250MethodSecurityConfiguration.java
new file mode 100644
index 00000000000..9b3207116c5
--- /dev/null
+++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/Jsr250MethodSecurityConfiguration.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2002-2021 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.aop.Advisor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Role;
+import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
+import org.springframework.security.authorization.method.Jsr250AuthorizationManager;
+import org.springframework.security.config.core.GrantedAuthorityDefaults;
+
+/**
+ * {@link Configuration} for enabling JSR-250 Spring Security Method Security.
+ *
+ * @author Evgeniy Cheban
+ * @author Josh Cummings
+ * @see EnableMethodSecurity
+ * @since 5.6
+ */
+@Configuration(proxyBeanMethods = false)
+@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+final class Jsr250MethodSecurityConfiguration {
+
+ private final Jsr250AuthorizationManager jsr250AuthorizationManager = new Jsr250AuthorizationManager();
+
+ @Bean
+ @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+ Advisor jsr250AuthorizationMethodInterceptor() {
+ return AuthorizationManagerBeforeMethodInterceptor.jsr250(this.jsr250AuthorizationManager);
+ }
+
+ @Autowired(required = false)
+ void setGrantedAuthorityDefaults(GrantedAuthorityDefaults grantedAuthorityDefaults) {
+ this.jsr250AuthorizationManager.setRolePrefix(grantedAuthorityDefaults.getRolePrefix());
+ }
+
+}
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
new file mode 100644
index 00000000000..0443a03e1c6
--- /dev/null
+++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodSecuritySelector.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2002-2021 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.springframework.context.annotation.AdviceMode;
+import org.springframework.context.annotation.AdviceModeImportSelector;
+import org.springframework.context.annotation.AutoProxyRegistrar;
+import org.springframework.context.annotation.ImportSelector;
+import org.springframework.core.type.AnnotationMetadata;
+import org.springframework.lang.NonNull;
+
+/**
+ * Dynamically determines which imports to include using the {@link EnableMethodSecurity}
+ * annotation.
+ *
+ * @author Evgeniy Cheban
+ * @author Josh Cummings
+ * @since 5.6
+ */
+final class MethodSecuritySelector implements ImportSelector {
+
+ private final ImportSelector autoProxy = new AutoProxyRegistrarSelector();
+
+ @Override
+ public String[] selectImports(@NonNull AnnotationMetadata importMetadata) {
+ if (!importMetadata.hasAnnotation(EnableMethodSecurity.class.getName())) {
+ return new String[0];
+ }
+ EnableMethodSecurity annotation = importMetadata.getAnnotations().get(EnableMethodSecurity.class).synthesize();
+ List imports = new ArrayList<>(Arrays.asList(this.autoProxy.selectImports(importMetadata)));
+ if (annotation.prePostEnabled()) {
+ imports.add(PrePostMethodSecurityConfiguration.class.getName());
+ }
+ if (annotation.securedEnabled()) {
+ imports.add(SecuredMethodSecurityConfiguration.class.getName());
+ }
+ if (annotation.jsr250Enabled()) {
+ imports.add(Jsr250MethodSecurityConfiguration.class.getName());
+ }
+ return imports.toArray(new String[0]);
+ }
+
+ private static final class AutoProxyRegistrarSelector extends AdviceModeImportSelector {
+
+ private static final String[] IMPORTS = new String[] { AutoProxyRegistrar.class.getName() };
+
+ @Override
+ protected String[] selectImports(@NonNull AdviceMode adviceMode) {
+ if (adviceMode == AdviceMode.PROXY) {
+ return IMPORTS;
+ }
+ throw new IllegalStateException("AdviceMode '" + adviceMode + "' is not supported");
+ }
+
+ }
+
+}
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
new file mode 100644
index 00000000000..3a3bd01d38b
--- /dev/null
+++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2002-2021 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.aop.Advisor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Role;
+import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
+import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
+import org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor;
+import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
+import org.springframework.security.authorization.method.PostAuthorizeAuthorizationManager;
+import org.springframework.security.authorization.method.PostFilterAuthorizationMethodInterceptor;
+import org.springframework.security.authorization.method.PreAuthorizeAuthorizationManager;
+import org.springframework.security.authorization.method.PreFilterAuthorizationMethodInterceptor;
+import org.springframework.security.config.core.GrantedAuthorityDefaults;
+
+/**
+ * Base {@link Configuration} for enabling Spring Security Method Security.
+ *
+ * @author Evgeniy Cheban
+ * @author Josh Cummings
+ * @see EnableMethodSecurity
+ * @since 5.6
+ */
+@Configuration(proxyBeanMethods = false)
+@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+final class PrePostMethodSecurityConfiguration {
+
+ private final PreFilterAuthorizationMethodInterceptor preFilterAuthorizationMethodInterceptor = new PreFilterAuthorizationMethodInterceptor();
+
+ private final PreAuthorizeAuthorizationManager preAuthorizeAuthorizationManager = new PreAuthorizeAuthorizationManager();
+
+ private final PostAuthorizeAuthorizationManager postAuthorizeAuthorizationManager = new PostAuthorizeAuthorizationManager();
+
+ private final PostFilterAuthorizationMethodInterceptor postFilterAuthorizationMethodInterceptor = new PostFilterAuthorizationMethodInterceptor();
+
+ private boolean customMethodSecurityExpressionHandler = false;
+
+ @Bean
+ @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+ Advisor preFilterAuthorizationMethodInterceptor() {
+ return this.preFilterAuthorizationMethodInterceptor;
+ }
+
+ @Bean
+ @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+ Advisor preAuthorizeAuthorizationMethodInterceptor() {
+ return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(this.preAuthorizeAuthorizationManager);
+ }
+
+ @Bean
+ @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+ Advisor postAuthorizeAuthorizationMethodInterceptor() {
+ return AuthorizationManagerAfterMethodInterceptor.postAuthorize(this.postAuthorizeAuthorizationManager);
+ }
+
+ @Bean
+ @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+ Advisor postFilterAuthorizationMethodInterceptor() {
+ return this.postFilterAuthorizationMethodInterceptor;
+ }
+
+ @Autowired(required = false)
+ void setMethodSecurityExpressionHandler(MethodSecurityExpressionHandler methodSecurityExpressionHandler) {
+ this.customMethodSecurityExpressionHandler = true;
+ this.preFilterAuthorizationMethodInterceptor.setExpressionHandler(methodSecurityExpressionHandler);
+ this.preAuthorizeAuthorizationManager.setExpressionHandler(methodSecurityExpressionHandler);
+ this.postAuthorizeAuthorizationManager.setExpressionHandler(methodSecurityExpressionHandler);
+ this.postFilterAuthorizationMethodInterceptor.setExpressionHandler(methodSecurityExpressionHandler);
+ }
+
+ @Autowired(required = false)
+ void setGrantedAuthorityDefaults(GrantedAuthorityDefaults grantedAuthorityDefaults) {
+ if (this.customMethodSecurityExpressionHandler) {
+ return;
+ }
+ DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
+ expressionHandler.setDefaultRolePrefix(grantedAuthorityDefaults.getRolePrefix());
+ this.preFilterAuthorizationMethodInterceptor.setExpressionHandler(expressionHandler);
+ this.preAuthorizeAuthorizationManager.setExpressionHandler(expressionHandler);
+ this.postAuthorizeAuthorizationManager.setExpressionHandler(expressionHandler);
+ this.postFilterAuthorizationMethodInterceptor.setExpressionHandler(expressionHandler);
+ }
+
+}
diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/SecuredMethodSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/SecuredMethodSecurityConfiguration.java
new file mode 100644
index 00000000000..5b7fc7358bb
--- /dev/null
+++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/SecuredMethodSecurityConfiguration.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2002-2021 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.aop.Advisor;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Role;
+import org.springframework.security.access.annotation.Secured;
+import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
+
+/**
+ * {@link Configuration} for enabling {@link Secured} Spring Security Method Security.
+ *
+ * @author Evgeniy Cheban
+ * @author Josh Cummings
+ * @see EnableMethodSecurity
+ * @since 5.6
+ */
+@Configuration(proxyBeanMethods = false)
+@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+final class SecuredMethodSecurityConfiguration {
+
+ @Bean
+ @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+ Advisor securedAuthorizationMethodInterceptor() {
+ return AuthorizationManagerBeforeMethodInterceptor.secured();
+ }
+
+}
diff --git a/config/src/main/java/org/springframework/security/config/method/MethodSecurityBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/method/MethodSecurityBeanDefinitionParser.java
new file mode 100644
index 00000000000..cbb17638729
--- /dev/null
+++ b/config/src/main/java/org/springframework/security/config/method/MethodSecurityBeanDefinitionParser.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright 2002-2018 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.method;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.w3c.dom.Element;
+
+import org.springframework.aop.config.AopNamespaceUtils;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.parsing.CompositeComponentDefinition;
+import org.springframework.beans.factory.support.BeanDefinitionBuilder;
+import org.springframework.beans.factory.xml.BeanDefinitionParser;
+import org.springframework.beans.factory.xml.ParserContext;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
+import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
+import org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor;
+import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
+import org.springframework.security.authorization.method.Jsr250AuthorizationManager;
+import org.springframework.security.authorization.method.PostAuthorizeAuthorizationManager;
+import org.springframework.security.authorization.method.PostFilterAuthorizationMethodInterceptor;
+import org.springframework.security.authorization.method.PreAuthorizeAuthorizationManager;
+import org.springframework.security.authorization.method.PreFilterAuthorizationMethodInterceptor;
+import org.springframework.security.config.Elements;
+import org.springframework.security.config.core.GrantedAuthorityDefaults;
+import org.springframework.util.xml.DomUtils;
+
+/**
+ * Processes the top-level "method-security" element.
+ *
+ * @author Josh Cummings
+ * @since 5.6
+ */
+public class MethodSecurityBeanDefinitionParser implements BeanDefinitionParser {
+
+ private final Log logger = LogFactory.getLog(getClass());
+
+ private static final String ATT_USE_JSR250 = "jsr250-enabled";
+
+ private static final String ATT_USE_SECURED = "secured-enabled";
+
+ private static final String ATT_USE_PREPOST = "pre-post-enabled";
+
+ private static final String ATT_REF = "ref";
+
+ @Override
+ public BeanDefinition parse(Element element, ParserContext pc) {
+ CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(element.getTagName(),
+ pc.extractSource(element));
+ pc.pushContainingComponent(compositeDef);
+ boolean prePostAnnotationsEnabled = !element.hasAttribute(ATT_USE_PREPOST)
+ || "true".equals(element.getAttribute(ATT_USE_PREPOST));
+ if (prePostAnnotationsEnabled) {
+ BeanDefinitionBuilder preFilterInterceptor = BeanDefinitionBuilder
+ .rootBeanDefinition(PreFilterAuthorizationMethodInterceptor.class)
+ .setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
+ BeanDefinitionBuilder preAuthorizeInterceptor = BeanDefinitionBuilder
+ .rootBeanDefinition(PreAuthorizeAuthorizationMethodInterceptor.class)
+ .setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
+ BeanDefinitionBuilder postAuthorizeInterceptor = BeanDefinitionBuilder
+ .rootBeanDefinition(PostAuthorizeAuthorizationMethodInterceptor.class)
+ .setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
+ BeanDefinitionBuilder postFilterInterceptor = BeanDefinitionBuilder
+ .rootBeanDefinition(PostFilterAuthorizationMethodInterceptor.class)
+ .setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
+ Element expressionHandlerElt = DomUtils.getChildElementByTagName(element, Elements.EXPRESSION_HANDLER);
+ if (expressionHandlerElt != null) {
+ String expressionHandlerRef = expressionHandlerElt.getAttribute(ATT_REF);
+ preFilterInterceptor.addPropertyReference("expressionHandler", expressionHandlerRef);
+ preAuthorizeInterceptor.addPropertyReference("expressionHandler", expressionHandlerRef);
+ postAuthorizeInterceptor.addPropertyReference("expressionHandler", expressionHandlerRef);
+ postFilterInterceptor.addPropertyReference("expressionHandler", expressionHandlerRef);
+ }
+ else {
+ BeanDefinition expressionHandler = BeanDefinitionBuilder
+ .rootBeanDefinition(MethodSecurityExpressionHandlerBean.class).getBeanDefinition();
+ preFilterInterceptor.addPropertyValue("expressionHandler", expressionHandler);
+ preAuthorizeInterceptor.addPropertyValue("expressionHandler", expressionHandler);
+ postAuthorizeInterceptor.addPropertyValue("expressionHandler", expressionHandler);
+ postFilterInterceptor.addPropertyValue("expressionHandler", expressionHandler);
+ }
+ pc.getRegistry().registerBeanDefinition("preFilterAuthorizationMethodInterceptor",
+ preFilterInterceptor.getBeanDefinition());
+ pc.getRegistry().registerBeanDefinition("preAuthorizeAuthorizationMethodInterceptor",
+ preAuthorizeInterceptor.getBeanDefinition());
+ pc.getRegistry().registerBeanDefinition("postAuthorizeAuthorizationMethodInterceptor",
+ postAuthorizeInterceptor.getBeanDefinition());
+ pc.getRegistry().registerBeanDefinition("postFilterAuthorizationMethodInterceptor",
+ postFilterInterceptor.getBeanDefinition());
+ }
+ boolean securedEnabled = "true".equals(element.getAttribute(ATT_USE_SECURED));
+ if (securedEnabled) {
+ BeanDefinitionBuilder securedInterceptor = BeanDefinitionBuilder
+ .rootBeanDefinition(AuthorizationManagerBeforeMethodInterceptor.class)
+ .setRole(BeanDefinition.ROLE_INFRASTRUCTURE).setFactoryMethod("secured");
+ pc.getRegistry().registerBeanDefinition("securedAuthorizationMethodInterceptor",
+ securedInterceptor.getBeanDefinition());
+ }
+ boolean jsr250Enabled = "true".equals(element.getAttribute(ATT_USE_JSR250));
+ if (jsr250Enabled) {
+ BeanDefinitionBuilder jsr250Interceptor = BeanDefinitionBuilder
+ .rootBeanDefinition(Jsr250AuthorizationMethodInterceptor.class)
+ .setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
+ pc.getRegistry().registerBeanDefinition("jsr250AuthorizationMethodInterceptor",
+ jsr250Interceptor.getBeanDefinition());
+ }
+ AopNamespaceUtils.registerAutoProxyCreatorIfNecessary(pc, element);
+ pc.popAndRegisterContainingComponent();
+ return null;
+ }
+
+ public static final class MethodSecurityExpressionHandlerBean
+ implements FactoryBean, ApplicationContextAware {
+
+ private final DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
+
+ @Override
+ public MethodSecurityExpressionHandler getObject() {
+ return this.expressionHandler;
+ }
+
+ @Override
+ public Class> getObjectType() {
+ return MethodSecurityExpressionHandler.class;
+ }
+
+ @Override
+ public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+ String[] grantedAuthorityDefaultsBeanNames = applicationContext
+ .getBeanNamesForType(GrantedAuthorityDefaults.class);
+ if (grantedAuthorityDefaultsBeanNames.length == 1) {
+ GrantedAuthorityDefaults grantedAuthorityDefaults = applicationContext
+ .getBean(grantedAuthorityDefaultsBeanNames[0], GrantedAuthorityDefaults.class);
+ this.expressionHandler.setDefaultRolePrefix(grantedAuthorityDefaults.getRolePrefix());
+ }
+ }
+
+ }
+
+ public static final class Jsr250AuthorizationMethodInterceptor
+ implements FactoryBean, ApplicationContextAware {
+
+ private final Jsr250AuthorizationManager manager = new Jsr250AuthorizationManager();
+
+ @Override
+ public AuthorizationManagerBeforeMethodInterceptor getObject() {
+ return AuthorizationManagerBeforeMethodInterceptor.jsr250(this.manager);
+ }
+
+ @Override
+ public Class> getObjectType() {
+ return AuthorizationManagerBeforeMethodInterceptor.class;
+ }
+
+ @Override
+ public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+ String[] grantedAuthorityDefaultsBeanNames = applicationContext
+ .getBeanNamesForType(GrantedAuthorityDefaults.class);
+ if (grantedAuthorityDefaultsBeanNames.length == 1) {
+ GrantedAuthorityDefaults grantedAuthorityDefaults = applicationContext
+ .getBean(grantedAuthorityDefaultsBeanNames[0], GrantedAuthorityDefaults.class);
+ this.manager.setRolePrefix(grantedAuthorityDefaults.getRolePrefix());
+ }
+ }
+
+ }
+
+ public static final class PreAuthorizeAuthorizationMethodInterceptor
+ implements FactoryBean {
+
+ private final PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager();
+
+ @Override
+ public AuthorizationManagerBeforeMethodInterceptor getObject() {
+ return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(this.manager);
+ }
+
+ @Override
+ public Class> getObjectType() {
+ return AuthorizationManagerBeforeMethodInterceptor.class;
+ }
+
+ public void setExpressionHandler(MethodSecurityExpressionHandler expressionHandler) {
+ this.manager.setExpressionHandler(expressionHandler);
+ }
+
+ }
+
+ public static final class PostAuthorizeAuthorizationMethodInterceptor
+ implements FactoryBean {
+
+ private final PostAuthorizeAuthorizationManager manager = new PostAuthorizeAuthorizationManager();
+
+ @Override
+ public AuthorizationManagerAfterMethodInterceptor getObject() {
+ return AuthorizationManagerAfterMethodInterceptor.postAuthorize(this.manager);
+ }
+
+ @Override
+ public Class> getObjectType() {
+ return AuthorizationManagerAfterMethodInterceptor.class;
+ }
+
+ public void setExpressionHandler(MethodSecurityExpressionHandler expressionHandler) {
+ this.manager.setExpressionHandler(expressionHandler);
+ }
+
+ }
+
+}
diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-5.6.rnc b/config/src/main/resources/org/springframework/security/config/spring-security-5.6.rnc
index 4d94b28a54b..72f0eb673b8 100644
--- a/config/src/main/resources/org/springframework/security/config/spring-security-5.6.rnc
+++ b/config/src/main/resources/org/springframework/security/config/spring-security-5.6.rnc
@@ -196,6 +196,22 @@ msmds.attlist &= id?
msmds.attlist &= use-expressions?
+method-security =
+ ## Provides method security for all beans registered in the Spring application context. Specifically, beans will be scanned for matches with Spring Security annotations. Where there is a match, the beans will automatically be proxied and security authorization applied to the methods accordingly. Interceptors are invoked in the order specified in AuthorizationInterceptorsOrder. Use can create your own interceptors using Spring AOP.
+ element method-security {method-security.attlist, expression-handler?}
+method-security.attlist &=
+ ## Specifies whether the use of Spring Security's pre and post invocation annotations (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) should be enabled for this application context. Defaults to "true".
+ attribute pre-post-enabled {xsd:boolean}?
+method-security.attlist &=
+ ## Specifies whether the use of Spring Security's @Secured annotations should be enabled for this application context. Defaults to "false".
+ attribute secured-enabled {xsd:boolean}?
+method-security.attlist &=
+ ## Specifies whether JSR-250 style attributes are to be used (for example "RolesAllowed"). This will require the javax.annotation.security classes on the classpath. Defaults to "false".
+ attribute jsr250-enabled {xsd:boolean}?
+method-security.attlist &=
+ ## If true, class-based proxying will be used instead of interface-based proxying.
+ attribute proxy-target-class {xsd:boolean}?
+
global-method-security =
## Provides method security for all beans registered in the Spring application context. Specifically, beans will be scanned for matches with the ordered list of "protect-pointcut" sub-elements, Spring Security annotations and/or. Where there is a match, the beans will automatically be proxied and security authorization applied to the methods accordingly. If you use and enable all four sources of method security metadata (ie "protect-pointcut" declarations, expression annotations, @Secured and also JSR250 security annotations), the metadata sources will be queried in that order. In practical terms, this enables you to use XML to override method security metadata expressed in annotations. If using annotations, the order of precedence is EL-based (@PreAuthorize etc.), @Secured and finally JSR-250.
element global-method-security {global-method-security.attlist, (pre-post-annotation-handling | expression-handler)?, protect-pointcut*, after-invocation-provider*}
diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-5.6.xsd b/config/src/main/resources/org/springframework/security/config/spring-security-5.6.xsd
index 74bb4fe0191..e8004b3efe0 100644
--- a/config/src/main/resources/org/springframework/security/config/spring-security-5.6.xsd
+++ b/config/src/main/resources/org/springframework/security/config/spring-security-5.6.xsd
@@ -595,6 +595,63 @@
+
+
+ Provides method security for all beans registered in the Spring application context.
+ Specifically, beans will be scanned for matches with Spring Security annotations. Where
+ there is a match, the beans will automatically be proxied and security authorization
+ applied to the methods accordingly. Interceptors are invoked in the order specified in
+ AuthorizationInterceptorsOrder. Use can create your own interceptors using Spring AOP.
+
+
+
+
+
+
+ Defines the SecurityExpressionHandler instance which will be used if expression-based
+ access-control is enabled. A default implementation (with no ACL support) will be used if
+ not supplied.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Specifies whether the use of Spring Security's pre and post invocation annotations
+ (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) should be enabled for this
+ application context. Defaults to "true".
+
+
+
+
+
+ Specifies whether the use of Spring Security's @Secured annotations should be enabled for
+ this application context. Defaults to "false".
+
+
+
+
+
+ Specifies whether JSR-250 style attributes are to be used (for example "RolesAllowed").
+ This will require the javax.annotation.security classes on the classpath. Defaults to
+ "false".
+
+
+
+
+
+ If true, class-based proxying will be used instead of interface-based proxying.
+
+
+
+
Provides method security for all beans registered in the Spring application context.
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 525ce2a4771..b3675705c03 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
@@ -16,12 +16,16 @@
package org.springframework.security.config.annotation.method.configuration;
+import java.util.List;
+
import javax.annotation.security.DenyAll;
import javax.annotation.security.PermitAll;
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.core.Authentication;
import org.springframework.security.core.parameters.P;
@@ -69,4 +73,15 @@ public interface MethodSecurityService {
@PostAuthorize("#o?.contains('grant')")
String postAnnotation(@P("o") String object);
+ @PreFilter("filterObject.length > 3")
+ @PreAuthorize("hasRole('ADMIN')")
+ @Secured("ROLE_USER")
+ @PostFilter("filterObject.length > 5")
+ @PostAuthorize("returnObject.size == 2")
+ List manyAnnotations(List array);
+
+ @RequireUserRole
+ @RequireAdminRole
+ void repeatedAnnotations();
+
}
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 94a05216bc1..cab0631af7a 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
@@ -16,6 +16,8 @@
package org.springframework.security.config.annotation.method.configuration;
+import java.util.List;
+
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
@@ -86,4 +88,13 @@ public String postAnnotation(String object) {
return null;
}
+ @Override
+ public List manyAnnotations(List object) {
+ return object;
+ }
+
+ @Override
+ public void repeatedAnnotations() {
+ }
+
}
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
new file mode 100644
index 00000000000..aa967cbe5d7
--- /dev/null
+++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java
@@ -0,0 +1,475 @@
+/*
+ * Copyright 2002-2021 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 java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.aopalliance.intercept.MethodInterceptor;
+import org.aopalliance.intercept.MethodInvocation;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.springframework.aop.Advisor;
+import org.springframework.aop.support.DefaultPointcutAdvisor;
+import org.springframework.aop.support.JdkRegexpMethodPointcut;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Role;
+import org.springframework.core.annotation.AnnotationConfigurationException;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.access.PermissionEvaluator;
+import org.springframework.security.access.annotation.BusinessService;
+import org.springframework.security.access.annotation.BusinessServiceImpl;
+import org.springframework.security.access.annotation.ExpressionProtectedBusinessServiceImpl;
+import org.springframework.security.access.annotation.Jsr250BusinessServiceImpl;
+import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
+import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
+import org.springframework.security.authorization.AuthorizationDecision;
+import org.springframework.security.authorization.AuthorizationManager;
+import org.springframework.security.authorization.method.AuthorizationInterceptorsOrder;
+import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
+import org.springframework.security.config.core.GrantedAuthorityDefaults;
+import org.springframework.security.config.test.SpringTestRule;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners;
+import org.springframework.security.test.context.support.WithAnonymousUser;
+import org.springframework.security.test.context.support.WithMockUser;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+
+/**
+ * Tests for {@link PrePostMethodSecurityConfiguration}.
+ *
+ * @author Evgeniy Cheban
+ * @author Josh Cummings
+ */
+@RunWith(SpringRunner.class)
+@SecurityTestExecutionListeners
+public class PrePostMethodSecurityConfigurationTests {
+
+ @Rule
+ public final SpringTestRule spring = new SpringTestRule();
+
+ @Autowired(required = false)
+ MethodSecurityService methodSecurityService;
+
+ @Autowired(required = false)
+ BusinessService businessService;
+
+ @WithMockUser(roles = "ADMIN")
+ @Test
+ public void preAuthorizeWhenRoleAdminThenAccessDeniedException() {
+ this.spring.register(MethodSecurityServiceConfig.class).autowire();
+ assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.methodSecurityService::preAuthorize)
+ .withMessage("Access Denied");
+ }
+
+ @WithAnonymousUser
+ @Test
+ public void preAuthorizePermitAllWhenRoleAnonymousThenPasses() {
+ this.spring.register(MethodSecurityServiceConfig.class).autowire();
+ String result = this.methodSecurityService.preAuthorizePermitAll();
+ assertThat(result).isNull();
+ }
+
+ @WithAnonymousUser
+ @Test
+ public void preAuthorizeNotAnonymousWhenRoleAnonymousThenAccessDeniedException() {
+ this.spring.register(MethodSecurityServiceConfig.class).autowire();
+ assertThatExceptionOfType(AccessDeniedException.class)
+ .isThrownBy(this.methodSecurityService::preAuthorizeNotAnonymous).withMessage("Access Denied");
+ }
+
+ @WithMockUser
+ @Test
+ public void preAuthorizeNotAnonymousWhenRoleUserThenPasses() {
+ this.spring.register(MethodSecurityServiceConfig.class).autowire();
+ this.methodSecurityService.preAuthorizeNotAnonymous();
+ }
+
+ @WithMockUser
+ @Test
+ public void securedWhenRoleUserThenAccessDeniedException() {
+ this.spring.register(MethodSecurityServiceEnabledConfig.class).autowire();
+ assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.methodSecurityService::secured)
+ .withMessage("Access Denied");
+ }
+
+ @WithMockUser(roles = "ADMIN")
+ @Test
+ public void securedWhenRoleAdminThenPasses() {
+ this.spring.register(MethodSecurityServiceConfig.class).autowire();
+ String result = this.methodSecurityService.secured();
+ assertThat(result).isNull();
+ }
+
+ @WithMockUser(roles = "ADMIN")
+ @Test
+ public void securedUserWhenRoleAdminThenAccessDeniedException() {
+ this.spring.register(MethodSecurityServiceEnabledConfig.class).autowire();
+ assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.methodSecurityService::securedUser)
+ .withMessage("Access Denied");
+ }
+
+ @WithMockUser
+ @Test
+ public void securedUserWhenRoleUserThenPasses() {
+ this.spring.register(MethodSecurityServiceConfig.class).autowire();
+ String result = this.methodSecurityService.securedUser();
+ assertThat(result).isNull();
+ }
+
+ @WithMockUser
+ @Test
+ public void preAuthorizeAdminWhenRoleUserThenAccessDeniedException() {
+ this.spring.register(MethodSecurityServiceConfig.class).autowire();
+ assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.methodSecurityService::preAuthorizeAdmin)
+ .withMessage("Access Denied");
+ }
+
+ @WithMockUser(roles = "ADMIN")
+ @Test
+ public void preAuthorizeAdminWhenRoleAdminThenPasses() {
+ this.spring.register(MethodSecurityServiceConfig.class).autowire();
+ this.methodSecurityService.preAuthorizeAdmin();
+ }
+
+ @WithMockUser(authorities = "PREFIX_ADMIN")
+ @Test
+ public void preAuthorizeAdminWhenRoleAdminAndCustomPrefixThenPasses() {
+ this.spring.register(CustomGrantedAuthorityDefaultsConfig.class, MethodSecurityServiceConfig.class).autowire();
+ this.methodSecurityService.preAuthorizeAdmin();
+ }
+
+ @WithMockUser
+ @Test
+ public void postHasPermissionWhenParameterIsNotGrantThenAccessDeniedException() {
+ this.spring.register(CustomPermissionEvaluatorConfig.class, MethodSecurityServiceConfig.class).autowire();
+ assertThatExceptionOfType(AccessDeniedException.class)
+ .isThrownBy(() -> this.methodSecurityService.postHasPermission("deny")).withMessage("Access Denied");
+ }
+
+ @WithMockUser
+ @Test
+ public void postHasPermissionWhenParameterIsGrantThenPasses() {
+ this.spring.register(CustomPermissionEvaluatorConfig.class, MethodSecurityServiceConfig.class).autowire();
+ String result = this.methodSecurityService.postHasPermission("grant");
+ assertThat(result).isNull();
+ }
+
+ @WithMockUser
+ @Test
+ public void postAnnotationWhenParameterIsNotGrantThenAccessDeniedException() {
+ this.spring.register(MethodSecurityServiceConfig.class).autowire();
+ assertThatExceptionOfType(AccessDeniedException.class)
+ .isThrownBy(() -> this.methodSecurityService.postAnnotation("deny")).withMessage("Access Denied");
+ }
+
+ @WithMockUser
+ @Test
+ public void postAnnotationWhenParameterIsGrantThenPasses() {
+ this.spring.register(MethodSecurityServiceConfig.class).autowire();
+ String result = this.methodSecurityService.postAnnotation("grant");
+ assertThat(result).isNull();
+ }
+
+ @WithMockUser("bob")
+ @Test
+ public void methodReturningAListWhenPrePostFiltersConfiguredThenFiltersList() {
+ this.spring.register(BusinessServiceConfig.class).autowire();
+ List names = new ArrayList<>();
+ names.add("bob");
+ names.add("joe");
+ names.add("sam");
+ List> result = this.businessService.methodReturningAList(names);
+ assertThat(result).hasSize(1);
+ assertThat(result.get(0)).isEqualTo("bob");
+ }
+
+ @WithMockUser("bob")
+ @Test
+ public void methodReturningAnArrayWhenPostFilterConfiguredThenFiltersArray() {
+ this.spring.register(BusinessServiceConfig.class).autowire();
+ List names = new ArrayList<>();
+ names.add("bob");
+ names.add("joe");
+ names.add("sam");
+ Object[] result = this.businessService.methodReturningAnArray(names.toArray());
+ assertThat(result).hasSize(1);
+ assertThat(result[0]).isEqualTo("bob");
+ }
+
+ @WithMockUser("bob")
+ @Test
+ public void securedUserWhenCustomBeforeAdviceConfiguredAndNameBobThenPasses() {
+ this.spring.register(CustomAuthorizationManagerBeforeAdviceConfig.class, MethodSecurityServiceConfig.class)
+ .autowire();
+ String result = this.methodSecurityService.securedUser();
+ assertThat(result).isNull();
+ }
+
+ @WithMockUser("joe")
+ @Test
+ public void securedUserWhenCustomBeforeAdviceConfiguredAndNameNotBobThenAccessDeniedException() {
+ this.spring.register(CustomAuthorizationManagerBeforeAdviceConfig.class, MethodSecurityServiceConfig.class)
+ .autowire();
+ assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.methodSecurityService::securedUser)
+ .withMessage("Access Denied");
+ }
+
+ @WithMockUser("bob")
+ @Test
+ public void securedUserWhenCustomAfterAdviceConfiguredAndNameBobThenGranted() {
+ this.spring.register(CustomAuthorizationManagerAfterAdviceConfig.class, MethodSecurityServiceConfig.class)
+ .autowire();
+ String result = this.methodSecurityService.securedUser();
+ assertThat(result).isEqualTo("granted");
+ }
+
+ @WithMockUser("joe")
+ @Test
+ public void securedUserWhenCustomAfterAdviceConfiguredAndNameNotBobThenAccessDeniedException() {
+ this.spring.register(CustomAuthorizationManagerAfterAdviceConfig.class, MethodSecurityServiceConfig.class)
+ .autowire();
+ assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.methodSecurityService::securedUser)
+ .withMessage("Access Denied for User 'joe'");
+ }
+
+ @WithMockUser(roles = "ADMIN")
+ @Test
+ public void jsr250WhenRoleAdminThenAccessDeniedException() {
+ this.spring.register(MethodSecurityServiceEnabledConfig.class).autowire();
+ assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.methodSecurityService::jsr250)
+ .withMessage("Access Denied");
+ }
+
+ @WithAnonymousUser
+ @Test
+ public void jsr250PermitAllWhenRoleAnonymousThenPasses() {
+ this.spring.register(MethodSecurityServiceEnabledConfig.class).autowire();
+ String result = this.methodSecurityService.jsr250PermitAll();
+ assertThat(result).isNull();
+ }
+
+ @WithMockUser(roles = "ADMIN")
+ @Test
+ public void rolesAllowedUserWhenRoleAdminThenAccessDeniedException() {
+ this.spring.register(BusinessServiceConfig.class).autowire();
+ assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.businessService::rolesAllowedUser)
+ .withMessage("Access Denied");
+ }
+
+ @WithMockUser
+ @Test
+ public void rolesAllowedUserWhenRoleUserThenPasses() {
+ this.spring.register(BusinessServiceConfig.class).autowire();
+ this.businessService.rolesAllowedUser();
+ }
+
+ @WithMockUser(roles = { "ADMIN", "USER" })
+ @Test
+ public void manyAnnotationsWhenMeetsConditionsThenReturnsFilteredList() throws Exception {
+ List names = Arrays.asList("harold", "jonathan", "pete", "bo");
+ this.spring.register(MethodSecurityServiceEnabledConfig.class).autowire();
+ List filtered = this.methodSecurityService.manyAnnotations(new ArrayList<>(names));
+ assertThat(filtered).hasSize(2);
+ assertThat(filtered).containsExactly("harold", "jonathan");
+ }
+
+ // gh-4003
+ // gh-4103
+ @WithMockUser
+ @Test
+ public void manyAnnotationsWhenUserThenFails() {
+ List names = Arrays.asList("harold", "jonathan", "pete", "bo");
+ this.spring.register(MethodSecurityServiceEnabledConfig.class).autowire();
+ assertThatExceptionOfType(AccessDeniedException.class)
+ .isThrownBy(() -> this.methodSecurityService.manyAnnotations(new ArrayList<>(names)));
+ }
+
+ @WithMockUser
+ @Test
+ public void manyAnnotationsWhenShortListThenFails() {
+ List names = Arrays.asList("harold", "jonathan", "pete");
+ this.spring.register(MethodSecurityServiceEnabledConfig.class).autowire();
+ assertThatExceptionOfType(AccessDeniedException.class)
+ .isThrownBy(() -> this.methodSecurityService.manyAnnotations(new ArrayList<>(names)));
+ }
+
+ @WithMockUser(roles = "ADMIN")
+ @Test
+ public void manyAnnotationsWhenAdminThenFails() {
+ List names = Arrays.asList("harold", "jonathan", "pete", "bo");
+ this.spring.register(MethodSecurityServiceEnabledConfig.class).autowire();
+ assertThatExceptionOfType(AccessDeniedException.class)
+ .isThrownBy(() -> this.methodSecurityService.manyAnnotations(new ArrayList<>(names)));
+ }
+
+ // gh-3183
+ @Test
+ public void repeatedAnnotationsWhenPresentThenFails() {
+ this.spring.register(MethodSecurityServiceConfig.class).autowire();
+ assertThatExceptionOfType(AnnotationConfigurationException.class)
+ .isThrownBy(() -> this.methodSecurityService.repeatedAnnotations());
+ }
+
+ // gh-3183
+ @Test
+ public void repeatedJsr250AnnotationsWhenPresentThenFails() {
+ this.spring.register(Jsr250Config.class).autowire();
+ assertThatExceptionOfType(AnnotationConfigurationException.class)
+ .isThrownBy(() -> this.businessService.repeatedAnnotations());
+ }
+
+ // gh-3183
+ @Test
+ public void repeatedSecuredAnnotationsWhenPresentThenFails() {
+ this.spring.register(SecuredConfig.class).autowire();
+ assertThatExceptionOfType(AnnotationConfigurationException.class)
+ .isThrownBy(() -> this.businessService.repeatedAnnotations());
+ }
+
+ @EnableMethodSecurity
+ static class MethodSecurityServiceConfig {
+
+ @Bean
+ MethodSecurityService methodSecurityService() {
+ return new MethodSecurityServiceImpl();
+ }
+
+ }
+
+ @EnableMethodSecurity(jsr250Enabled = true)
+ static class BusinessServiceConfig {
+
+ @Bean
+ BusinessService businessService() {
+ return new ExpressionProtectedBusinessServiceImpl();
+ }
+
+ }
+
+ @EnableMethodSecurity(prePostEnabled = false, securedEnabled = true)
+ static class SecuredConfig {
+
+ @Bean
+ BusinessService businessService() {
+ return new BusinessServiceImpl<>();
+ }
+
+ }
+
+ @EnableMethodSecurity(prePostEnabled = false, jsr250Enabled = true)
+ static class Jsr250Config {
+
+ @Bean
+ BusinessService businessService() {
+ return new Jsr250BusinessServiceImpl();
+ }
+
+ }
+
+ @EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true)
+ static class MethodSecurityServiceEnabledConfig {
+
+ @Bean
+ MethodSecurityService methodSecurityService() {
+ return new MethodSecurityServiceImpl();
+ }
+
+ }
+
+ @EnableMethodSecurity
+ static class CustomPermissionEvaluatorConfig {
+
+ @Bean
+ MethodSecurityExpressionHandler methodSecurityExpressionHandler() {
+ DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
+ expressionHandler.setPermissionEvaluator(new PermissionEvaluator() {
+ @Override
+ public boolean hasPermission(Authentication authentication, Object targetDomainObject,
+ Object permission) {
+ return "grant".equals(targetDomainObject);
+ }
+
+ @Override
+ public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType,
+ Object permission) {
+ throw new UnsupportedOperationException();
+ }
+ });
+ return expressionHandler;
+ }
+
+ }
+
+ @EnableMethodSecurity
+ static class CustomGrantedAuthorityDefaultsConfig {
+
+ @Bean
+ GrantedAuthorityDefaults grantedAuthorityDefaults() {
+ return new GrantedAuthorityDefaults("PREFIX_");
+ }
+
+ }
+
+ @EnableMethodSecurity
+ static class CustomAuthorizationManagerBeforeAdviceConfig {
+
+ @Bean
+ @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+ Advisor customBeforeAdvice() {
+ JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut();
+ pointcut.setPattern(".*MethodSecurityServiceImpl.*securedUser");
+ AuthorizationManager authorizationManager = (a,
+ o) -> new AuthorizationDecision("bob".equals(a.get().getName()));
+ return new AuthorizationManagerBeforeMethodInterceptor(pointcut, authorizationManager);
+ }
+
+ }
+
+ @EnableMethodSecurity
+ static class CustomAuthorizationManagerAfterAdviceConfig {
+
+ @Bean
+ @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+ Advisor customAfterAdvice() {
+ JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut();
+ pointcut.setPattern(".*MethodSecurityServiceImpl.*securedUser");
+ MethodInterceptor interceptor = (mi) -> {
+ Authentication auth = SecurityContextHolder.getContext().getAuthentication();
+ if ("bob".equals(auth.getName())) {
+ return "granted";
+ }
+ throw new AccessDeniedException("Access Denied for User '" + auth.getName() + "'");
+ };
+ DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, interceptor);
+ advisor.setOrder(AuthorizationInterceptorsOrder.POST_FILTER.getOrder() + 1);
+ return advisor;
+ }
+
+ }
+
+}
diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/RequireAdminRole.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/RequireAdminRole.java
new file mode 100644
index 00000000000..5415bdb04a1
--- /dev/null
+++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/RequireAdminRole.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2002-2021 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 java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import org.springframework.security.access.prepost.PreAuthorize;
+
+@Retention(RetentionPolicy.RUNTIME)
+@PreAuthorize("hasRole('ADMIN')")
+public @interface RequireAdminRole {
+
+}
diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/RequireUserRole.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/RequireUserRole.java
new file mode 100644
index 00000000000..950033dbcb9
--- /dev/null
+++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/RequireUserRole.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2002-2021 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 java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import org.springframework.security.access.prepost.PreAuthorize;
+
+@Retention(RetentionPolicy.RUNTIME)
+@PreAuthorize("hasRole('USER')")
+public @interface RequireUserRole {
+
+}
diff --git a/config/src/test/java/org/springframework/security/config/doc/XsdDocumentedTests.java b/config/src/test/java/org/springframework/security/config/doc/XsdDocumentedTests.java
index 506386e98d2..fc57a11a188 100644
--- a/config/src/test/java/org/springframework/security/config/doc/XsdDocumentedTests.java
+++ b/config/src/test/java/org/springframework/security/config/doc/XsdDocumentedTests.java
@@ -52,7 +52,6 @@ public class XsdDocumentedTests {
"nsa-authentication",
"nsa-websocket-security",
"nsa-ldap",
- "nsa-method-security",
"nsa-web",
// deprecated and for removal
"nsa-frame-options-strategy",
diff --git a/config/src/test/java/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests.java
new file mode 100644
index 00000000000..5a8b69457a1
--- /dev/null
+++ b/config/src/test/java/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests.java
@@ -0,0 +1,385 @@
+/*
+ * Copyright 2002-2017 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.method;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Supplier;
+
+import org.aopalliance.intercept.MethodInterceptor;
+import org.aopalliance.intercept.MethodInvocation;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.annotation.AnnotationConfigurationException;
+import org.springframework.lang.Nullable;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.access.PermissionEvaluator;
+import org.springframework.security.access.annotation.BusinessService;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.authorization.AuthorizationDecision;
+import org.springframework.security.authorization.AuthorizationManager;
+import org.springframework.security.config.annotation.method.configuration.MethodSecurityService;
+import org.springframework.security.config.test.SpringTestRule;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners;
+import org.springframework.security.test.context.support.WithAnonymousUser;
+import org.springframework.security.test.context.support.WithMockUser;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+
+/**
+ * @author Josh Cummings
+ */
+@RunWith(SpringRunner.class)
+@SecurityTestExecutionListeners
+public class MethodSecurityBeanDefinitionParserTests {
+
+ private static final String CONFIG_LOCATION_PREFIX = "classpath:org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests";
+
+ private final UsernamePasswordAuthenticationToken bob = new UsernamePasswordAuthenticationToken("bob",
+ "bobspassword");
+
+ @Autowired(required = false)
+ MethodSecurityService methodSecurityService;
+
+ @Autowired(required = false)
+ BusinessService businessService;
+
+ @Rule
+ public final SpringTestRule spring = new SpringTestRule();
+
+ @WithMockUser(roles = "ADMIN")
+ @Test
+ public void preAuthorizeWhenRoleAdminThenAccessDeniedException() {
+ this.spring.configLocations(xml("MethodSecurityService")).autowire();
+ assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.methodSecurityService::preAuthorize)
+ .withMessage("Access Denied");
+ }
+
+ @WithAnonymousUser
+ @Test
+ public void preAuthorizePermitAllWhenRoleAnonymousThenPasses() {
+ this.spring.configLocations(xml("MethodSecurityService")).autowire();
+ String result = this.methodSecurityService.preAuthorizePermitAll();
+ assertThat(result).isNull();
+ }
+
+ @WithAnonymousUser
+ @Test
+ public void preAuthorizeNotAnonymousWhenRoleAnonymousThenAccessDeniedException() {
+ this.spring.configLocations(xml("MethodSecurityService")).autowire();
+ assertThatExceptionOfType(AccessDeniedException.class)
+ .isThrownBy(this.methodSecurityService::preAuthorizeNotAnonymous).withMessage("Access Denied");
+ }
+
+ @WithMockUser
+ @Test
+ public void preAuthorizeNotAnonymousWhenRoleUserThenPasses() {
+ this.spring.configLocations(xml("MethodSecurityService")).autowire();
+ this.methodSecurityService.preAuthorizeNotAnonymous();
+ }
+
+ @WithMockUser
+ @Test
+ public void securedWhenRoleUserThenAccessDeniedException() {
+ this.spring.configLocations(xml("MethodSecurityServiceEnabled")).autowire();
+ assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.methodSecurityService::secured)
+ .withMessage("Access Denied");
+ }
+
+ @WithMockUser(roles = "ADMIN")
+ @Test
+ public void securedWhenRoleAdminThenPasses() {
+ this.spring.configLocations(xml("MethodSecurityService")).autowire();
+ String result = this.methodSecurityService.secured();
+ assertThat(result).isNull();
+ }
+
+ @WithMockUser(roles = "ADMIN")
+ @Test
+ public void securedUserWhenRoleAdminThenAccessDeniedException() {
+ this.spring.configLocations(xml("MethodSecurityServiceEnabled")).autowire();
+ assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.methodSecurityService::securedUser)
+ .withMessage("Access Denied");
+ }
+
+ @WithMockUser
+ @Test
+ public void securedUserWhenRoleUserThenPasses() {
+ this.spring.configLocations(xml("MethodSecurityService")).autowire();
+ String result = this.methodSecurityService.securedUser();
+ assertThat(result).isNull();
+ }
+
+ @WithMockUser
+ @Test
+ public void preAuthorizeAdminWhenRoleUserThenAccessDeniedException() {
+ this.spring.configLocations(xml("MethodSecurityService")).autowire();
+ assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.methodSecurityService::preAuthorizeAdmin)
+ .withMessage("Access Denied");
+ }
+
+ @WithMockUser(roles = "ADMIN")
+ @Test
+ public void preAuthorizeAdminWhenRoleAdminThenPasses() {
+ this.spring.configLocations(xml("MethodSecurityService")).autowire();
+ this.methodSecurityService.preAuthorizeAdmin();
+ }
+
+ @WithMockUser(authorities = "PREFIX_ADMIN")
+ @Test
+ public void preAuthorizeAdminWhenRoleAdminAndCustomPrefixThenPasses() {
+ this.spring.configLocations(xml("CustomGrantedAuthorityDefaults")).autowire();
+ this.methodSecurityService.preAuthorizeAdmin();
+ }
+
+ @WithMockUser
+ @Test
+ public void postHasPermissionWhenParameterIsNotGrantThenAccessDeniedException() {
+ this.spring.configLocations(xml("CustomPermissionEvaluator")).autowire();
+ assertThatExceptionOfType(AccessDeniedException.class)
+ .isThrownBy(() -> this.methodSecurityService.postHasPermission("deny")).withMessage("Access Denied");
+ }
+
+ @WithMockUser
+ @Test
+ public void postHasPermissionWhenParameterIsGrantThenPasses() {
+ this.spring.configLocations(xml("CustomPermissionEvaluator")).autowire();
+ String result = this.methodSecurityService.postHasPermission("grant");
+ assertThat(result).isNull();
+ }
+
+ @WithMockUser
+ @Test
+ public void postAnnotationWhenParameterIsNotGrantThenAccessDeniedException() {
+ this.spring.configLocations(xml("MethodSecurityService")).autowire();
+ assertThatExceptionOfType(AccessDeniedException.class)
+ .isThrownBy(() -> this.methodSecurityService.postAnnotation("deny")).withMessage("Access Denied");
+ }
+
+ @WithMockUser
+ @Test
+ public void postAnnotationWhenParameterIsGrantThenPasses() {
+ this.spring.configLocations(xml("MethodSecurityService")).autowire();
+ String result = this.methodSecurityService.postAnnotation("grant");
+ assertThat(result).isNull();
+ }
+
+ @WithMockUser("bob")
+ @Test
+ public void methodReturningAListWhenPrePostFiltersConfiguredThenFiltersList() {
+ this.spring.configLocations(xml("BusinessService")).autowire();
+ List names = new ArrayList<>();
+ names.add("bob");
+ names.add("joe");
+ names.add("sam");
+ List> result = this.businessService.methodReturningAList(names);
+ assertThat(result).hasSize(1);
+ assertThat(result.get(0)).isEqualTo("bob");
+ }
+
+ @WithMockUser("bob")
+ @Test
+ public void methodReturningAnArrayWhenPostFilterConfiguredThenFiltersArray() {
+ this.spring.configLocations(xml("BusinessService")).autowire();
+ List names = new ArrayList<>();
+ names.add("bob");
+ names.add("joe");
+ names.add("sam");
+ Object[] result = this.businessService.methodReturningAnArray(names.toArray());
+ assertThat(result).hasSize(1);
+ assertThat(result[0]).isEqualTo("bob");
+ }
+
+ @WithMockUser("bob")
+ @Test
+ public void securedUserWhenCustomBeforeAdviceConfiguredAndNameBobThenPasses() {
+ this.spring.configLocations(xml("CustomAuthorizationManagerBeforeAdvice")).autowire();
+ String result = this.methodSecurityService.securedUser();
+ assertThat(result).isNull();
+ }
+
+ @WithMockUser("joe")
+ @Test
+ public void securedUserWhenCustomBeforeAdviceConfiguredAndNameNotBobThenAccessDeniedException() {
+ this.spring.configLocations(xml("CustomAuthorizationManagerBeforeAdvice")).autowire();
+ assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.methodSecurityService::securedUser)
+ .withMessage("Access Denied");
+ }
+
+ @WithMockUser("bob")
+ @Test
+ public void securedUserWhenCustomAfterAdviceConfiguredAndNameBobThenGranted() {
+ this.spring.configLocations(xml("CustomAuthorizationManagerAfterAdvice")).autowire();
+ String result = this.methodSecurityService.securedUser();
+ assertThat(result).isEqualTo("granted");
+ }
+
+ @WithMockUser("joe")
+ @Test
+ public void securedUserWhenCustomAfterAdviceConfiguredAndNameNotBobThenAccessDeniedException() {
+ this.spring.configLocations(xml("CustomAuthorizationManagerAfterAdvice")).autowire();
+ assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.methodSecurityService::securedUser)
+ .withMessage("Access Denied for User 'joe'");
+ }
+
+ @WithMockUser(roles = "ADMIN")
+ @Test
+ public void jsr250WhenRoleAdminThenAccessDeniedException() {
+ this.spring.configLocations(xml("MethodSecurityServiceEnabled")).autowire();
+ assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.methodSecurityService::jsr250)
+ .withMessage("Access Denied");
+ }
+
+ @WithAnonymousUser
+ @Test
+ public void jsr250PermitAllWhenRoleAnonymousThenPasses() {
+ this.spring.configLocations(xml("MethodSecurityServiceEnabled")).autowire();
+ String result = this.methodSecurityService.jsr250PermitAll();
+ assertThat(result).isNull();
+ }
+
+ @WithMockUser(roles = "ADMIN")
+ @Test
+ public void rolesAllowedUserWhenRoleAdminThenAccessDeniedException() {
+ this.spring.configLocations(xml("BusinessService")).autowire();
+ assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.businessService::rolesAllowedUser)
+ .withMessage("Access Denied");
+ }
+
+ @WithMockUser
+ @Test
+ public void rolesAllowedUserWhenRoleUserThenPasses() {
+ this.spring.configLocations(xml("BusinessService")).autowire();
+ this.businessService.rolesAllowedUser();
+ }
+
+ @WithMockUser(roles = { "ADMIN", "USER" })
+ @Test
+ public void manyAnnotationsWhenMeetsConditionsThenReturnsFilteredList() throws Exception {
+ List names = Arrays.asList("harold", "jonathan", "pete", "bo");
+ this.spring.configLocations(xml("MethodSecurityServiceEnabled")).autowire();
+ List filtered = this.methodSecurityService.manyAnnotations(new ArrayList<>(names));
+ assertThat(filtered).hasSize(2);
+ assertThat(filtered).containsExactly("harold", "jonathan");
+ }
+
+ // gh-4003
+ // gh-4103
+ @WithMockUser
+ @Test
+ public void manyAnnotationsWhenUserThenFails() {
+ List names = Arrays.asList("harold", "jonathan", "pete", "bo");
+ this.spring.configLocations(xml("MethodSecurityServiceEnabled")).autowire();
+ assertThatExceptionOfType(AccessDeniedException.class)
+ .isThrownBy(() -> this.methodSecurityService.manyAnnotations(new ArrayList<>(names)));
+ }
+
+ @WithMockUser
+ @Test
+ public void manyAnnotationsWhenShortListThenFails() {
+ List names = Arrays.asList("harold", "jonathan", "pete");
+ this.spring.configLocations(xml("MethodSecurityServiceEnabled")).autowire();
+ assertThatExceptionOfType(AccessDeniedException.class)
+ .isThrownBy(() -> this.methodSecurityService.manyAnnotations(new ArrayList<>(names)));
+ }
+
+ @WithMockUser(roles = "ADMIN")
+ @Test
+ public void manyAnnotationsWhenAdminThenFails() {
+ List names = Arrays.asList("harold", "jonathan", "pete", "bo");
+ this.spring.configLocations(xml("MethodSecurityServiceEnabled")).autowire();
+ assertThatExceptionOfType(AccessDeniedException.class)
+ .isThrownBy(() -> this.methodSecurityService.manyAnnotations(new ArrayList<>(names)));
+ }
+
+ // gh-3183
+ @Test
+ public void repeatedAnnotationsWhenPresentThenFails() {
+ this.spring.configLocations(xml("MethodSecurityService")).autowire();
+ assertThatExceptionOfType(AnnotationConfigurationException.class)
+ .isThrownBy(() -> this.methodSecurityService.repeatedAnnotations());
+ }
+
+ // gh-3183
+ @Test
+ public void repeatedJsr250AnnotationsWhenPresentThenFails() {
+ this.spring.configLocations(xml("Jsr250")).autowire();
+ assertThatExceptionOfType(AnnotationConfigurationException.class)
+ .isThrownBy(() -> this.businessService.repeatedAnnotations());
+ }
+
+ // gh-3183
+ @Test
+ public void repeatedSecuredAnnotationsWhenPresentThenFails() {
+ this.spring.configLocations(xml("Secured")).autowire();
+ assertThatExceptionOfType(AnnotationConfigurationException.class)
+ .isThrownBy(() -> this.businessService.repeatedAnnotations());
+ }
+
+ private static String xml(String configName) {
+ return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml";
+ }
+
+ static class MyPermissionEvaluator implements PermissionEvaluator {
+
+ @Override
+ public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
+ return "grant".equals(targetDomainObject);
+ }
+
+ @Override
+ public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType,
+ Object permission) {
+ throw new UnsupportedOperationException();
+ }
+
+ }
+
+ static class MyAuthorizationManager implements AuthorizationManager {
+
+ @Override
+ public AuthorizationDecision check(Supplier authentication, MethodInvocation object) {
+ return new AuthorizationDecision("bob".equals(authentication.get().getName()));
+ }
+
+ }
+
+ static class MyAdvice implements MethodInterceptor {
+
+ @Nullable
+ @Override
+ public Object invoke(@NotNull MethodInvocation invocation) {
+ Authentication auth = SecurityContextHolder.getContext().getAuthentication();
+ if ("bob".equals(auth.getName())) {
+ return "granted";
+ }
+ throw new AccessDeniedException("Access Denied for User '" + auth.getName() + "'");
+ }
+
+ }
+
+}
diff --git a/config/src/test/resources/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests-BusinessService.xml b/config/src/test/resources/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests-BusinessService.xml
new file mode 100644
index 00000000000..4e845f4b3ec
--- /dev/null
+++ b/config/src/test/resources/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests-BusinessService.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/config/src/test/resources/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests-CustomAuthorizationManagerAfterAdvice.xml b/config/src/test/resources/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests-CustomAuthorizationManagerAfterAdvice.xml
new file mode 100644
index 00000000000..0af148c2ce1
--- /dev/null
+++ b/config/src/test/resources/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests-CustomAuthorizationManagerAfterAdvice.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/config/src/test/resources/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests-CustomAuthorizationManagerBeforeAdvice.xml b/config/src/test/resources/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests-CustomAuthorizationManagerBeforeAdvice.xml
new file mode 100644
index 00000000000..3887af41a9f
--- /dev/null
+++ b/config/src/test/resources/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests-CustomAuthorizationManagerBeforeAdvice.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/config/src/test/resources/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests-CustomGrantedAuthorityDefaults.xml b/config/src/test/resources/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests-CustomGrantedAuthorityDefaults.xml
new file mode 100644
index 00000000000..a2375a20419
--- /dev/null
+++ b/config/src/test/resources/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests-CustomGrantedAuthorityDefaults.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/config/src/test/resources/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests-CustomPermissionEvaluator.xml b/config/src/test/resources/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests-CustomPermissionEvaluator.xml
new file mode 100644
index 00000000000..2e791f752fe
--- /dev/null
+++ b/config/src/test/resources/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests-CustomPermissionEvaluator.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/config/src/test/resources/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests-Jsr250.xml b/config/src/test/resources/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests-Jsr250.xml
new file mode 100644
index 00000000000..87891eed1b6
--- /dev/null
+++ b/config/src/test/resources/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests-Jsr250.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/config/src/test/resources/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests-MethodSecurityService.xml b/config/src/test/resources/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests-MethodSecurityService.xml
new file mode 100644
index 00000000000..8c8bf9d9412
--- /dev/null
+++ b/config/src/test/resources/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests-MethodSecurityService.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/config/src/test/resources/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests-MethodSecurityServiceEnabled.xml b/config/src/test/resources/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests-MethodSecurityServiceEnabled.xml
new file mode 100644
index 00000000000..364899e139c
--- /dev/null
+++ b/config/src/test/resources/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests-MethodSecurityServiceEnabled.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/config/src/test/resources/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests-Secured.xml b/config/src/test/resources/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests-Secured.xml
new file mode 100644
index 00000000000..39df9573760
--- /dev/null
+++ b/config/src/test/resources/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests-Secured.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/core/src/main/java/org/springframework/security/authorization/AuthorityAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/AuthorityAuthorizationManager.java
index ea6f3d80272..7d245aed6ed 100644
--- a/core/src/main/java/org/springframework/security/authorization/AuthorityAuthorizationManager.java
+++ b/core/src/main/java/org/springframework/security/authorization/AuthorityAuthorizationManager.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2020 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -75,9 +75,22 @@ public static AuthorityAuthorizationManager hasAuthority(String authority
* @return the new instance
*/
public static AuthorityAuthorizationManager hasAnyRole(String... roles) {
+ return hasAnyRole(ROLE_PREFIX, roles);
+ }
+
+ /**
+ * Creates an instance of {@link AuthorityAuthorizationManager} with the provided
+ * authorities.
+ * @param rolePrefix the role prefix for roles
+ * @param roles the authorities to check for prefixed with rolePrefix
+ * @param the type of object being authorized
+ * @return the new instance
+ */
+ public static AuthorityAuthorizationManager hasAnyRole(String rolePrefix, String[] roles) {
+ Assert.notNull(rolePrefix, "rolePrefix cannot be null");
Assert.notEmpty(roles, "roles cannot be empty");
Assert.noNullElements(roles, "roles cannot contain null values");
- return hasAnyAuthority(toNamedRolesArray(roles));
+ return hasAnyAuthority(toNamedRolesArray(rolePrefix, roles));
}
/**
@@ -93,10 +106,10 @@ public static AuthorityAuthorizationManager hasAnyAuthority(String... aut
return new AuthorityAuthorizationManager<>(authorities);
}
- private static String[] toNamedRolesArray(String... roles) {
+ private static String[] toNamedRolesArray(String rolePrefix, String[] roles) {
String[] result = new String[roles.length];
for (int i = 0; i < roles.length; i++) {
- result[i] = ROLE_PREFIX + roles[i];
+ result[i] = rolePrefix + roles[i];
}
return result;
}
diff --git a/core/src/main/java/org/springframework/security/authorization/method/AbstractAuthorizationManagerRegistry.java b/core/src/main/java/org/springframework/security/authorization/method/AbstractAuthorizationManagerRegistry.java
new file mode 100644
index 00000000000..d40c51c1f6b
--- /dev/null
+++ b/core/src/main/java/org/springframework/security/authorization/method/AbstractAuthorizationManagerRegistry.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2002-2021 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.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.aopalliance.intercept.MethodInvocation;
+
+import org.springframework.aop.support.AopUtils;
+import org.springframework.core.MethodClassKey;
+import org.springframework.lang.NonNull;
+import org.springframework.security.authorization.AuthorizationManager;
+
+/**
+ * For internal use only, as this contract is likely to change
+ *
+ * @author Evgeniy Cheban
+ */
+abstract class AbstractAuthorizationManagerRegistry {
+
+ static final AuthorizationManager NULL_MANAGER = (a, o) -> null;
+
+ private final Map> cachedManagers = new ConcurrentHashMap<>();
+
+ /**
+ * Returns an {@link AuthorizationManager} for the {@link MethodInvocation}.
+ * @param methodInvocation the {@link MethodInvocation} to use
+ * @return an {@link AuthorizationManager} to use
+ */
+ final AuthorizationManager getManager(MethodInvocation methodInvocation) {
+ Method method = methodInvocation.getMethod();
+ Object target = methodInvocation.getThis();
+ Class> targetClass = (target != null) ? AopUtils.getTargetClass(target) : null;
+ MethodClassKey cacheKey = new MethodClassKey(method, targetClass);
+ return this.cachedManagers.computeIfAbsent(cacheKey, (k) -> resolveManager(method, targetClass));
+ }
+
+ /**
+ * Subclasses should implement this method to provide the non-null
+ * {@link AuthorizationManager} for the method and the target class.
+ * @param method the method
+ * @param targetClass the target class
+ * @return the non-null {@link AuthorizationManager}
+ */
+ @NonNull
+ abstract AuthorizationManager resolveManager(Method method, Class> targetClass);
+
+}
diff --git a/core/src/main/java/org/springframework/security/authorization/method/AbstractExpressionAttributeRegistry.java b/core/src/main/java/org/springframework/security/authorization/method/AbstractExpressionAttributeRegistry.java
new file mode 100644
index 00000000000..17defe9cde8
--- /dev/null
+++ b/core/src/main/java/org/springframework/security/authorization/method/AbstractExpressionAttributeRegistry.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2002-2021 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.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.aopalliance.intercept.MethodInvocation;
+
+import org.springframework.aop.support.AopUtils;
+import org.springframework.core.MethodClassKey;
+import org.springframework.lang.NonNull;
+
+/**
+ * For internal use only, as this contract is likely to change
+ *
+ * @author Evgeniy Cheban
+ */
+abstract class AbstractExpressionAttributeRegistry {
+
+ private final Map cachedAttributes = new ConcurrentHashMap<>();
+
+ /**
+ * Returns an {@link ExpressionAttribute} for the {@link MethodInvocation}.
+ * @param mi the {@link MethodInvocation} to use
+ * @return the {@link ExpressionAttribute} to use
+ */
+ final T getAttribute(MethodInvocation mi) {
+ Method method = mi.getMethod();
+ Object target = mi.getThis();
+ Class> targetClass = (target != null) ? AopUtils.getTargetClass(target) : null;
+ return getAttribute(method, targetClass);
+ }
+
+ /**
+ * Returns an {@link ExpressionAttribute} for the method and the target class.
+ * @param method the method
+ * @param targetClass the target class
+ * @return the {@link ExpressionAttribute} to use
+ */
+ final T getAttribute(Method method, Class> targetClass) {
+ MethodClassKey cacheKey = new MethodClassKey(method, targetClass);
+ return this.cachedAttributes.computeIfAbsent(cacheKey, (k) -> resolveAttribute(method, targetClass));
+ }
+
+ /**
+ * Subclasses should implement this method to provide the non-null
+ * {@link ExpressionAttribute} for the method and the target class.
+ * @param method the method
+ * @param targetClass the target class
+ * @return the non-null {@link ExpressionAttribute}
+ */
+ @NonNull
+ abstract T resolveAttribute(Method method, Class> targetClass);
+
+}
diff --git a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationAnnotationUtils.java b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationAnnotationUtils.java
new file mode 100644
index 00000000000..582436ba175
--- /dev/null
+++ b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationAnnotationUtils.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2002-2021 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.Annotation;
+import java.lang.reflect.Method;
+
+import org.springframework.core.annotation.AnnotationConfigurationException;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.core.annotation.MergedAnnotation;
+import org.springframework.core.annotation.MergedAnnotations;
+import org.springframework.core.annotation.RepeatableContainers;
+
+/**
+ * A wrapper around {@link AnnotationUtils} that checks for, and errors on, conflicting
+ * annotations. This is specifically important for Spring Security annotations which are
+ * not designed to be repeatable.
+ *
+ * There are numerous ways that two annotations of the same type may be attached to the
+ * same method. For example, a class may implement a method defined in two separate
+ * interfaces. If both of those interfaces have a `@PreAuthorize` annotation, then it's
+ * unclear which `@PreAuthorize` expression Spring Security should use.
+ *
+ * Another way is when one of Spring Security's annotations is used as a meta-annotation.
+ * In that case, two custom annotations can be declared, each with their own
+ * `@PreAuthorize` declaration. If both custom annotations are used on the same method,
+ * then it's unclear which `@PreAuthorize` expression Spring Security should use.
+ *
+ * @author Josh Cummings
+ */
+final class AuthorizationAnnotationUtils {
+
+ /**
+ * Perform an exhaustive search on the type hierarchy of the given {@link Method} for
+ * the annotation of type {@code annotationType}, including any annotations using
+ * {@code annotationType} as a meta-annotation.
+ *
+ * If more than one is found, then throw an error.
+ * @param method the method declaration to search from
+ * @param annotationType the annotation type to search for
+ * @return the unique instance of the annotation attributed to the method,
+ * {@code null} otherwise
+ * @throws AnnotationConfigurationException if more than one instance of the
+ * annotation is found
+ */
+ static A findUniqueAnnotation(Method method, Class annotationType) {
+ MergedAnnotations mergedAnnotations = MergedAnnotations.from(method,
+ MergedAnnotations.SearchStrategy.TYPE_HIERARCHY, RepeatableContainers.none());
+ if (hasDuplicate(mergedAnnotations, annotationType)) {
+ throw new AnnotationConfigurationException("Found more than one annotation of type " + annotationType
+ + " attributed to " + method
+ + " Please remove the duplicate annotations and publish a bean to handle your authorization logic.");
+ }
+ return AnnotationUtils.findAnnotation(method, annotationType);
+ }
+
+ /**
+ * Perform an exhaustive search on the type hierarchy of the given {@link Class} for
+ * the annotation of type {@code annotationType}, including any annotations using
+ * {@code annotationType} as a meta-annotation.
+ *
+ * If more than one is found, then throw an error.
+ * @param type the type to search from
+ * @param annotationType the annotation type to search for
+ * @return the unique instance of the annotation attributed to the method,
+ * {@code null} otherwise
+ * @throws AnnotationConfigurationException if more than one instance of the
+ * annotation is found
+ */
+ static A findUniqueAnnotation(Class> type, Class annotationType) {
+ MergedAnnotations mergedAnnotations = MergedAnnotations.from(type,
+ MergedAnnotations.SearchStrategy.TYPE_HIERARCHY, RepeatableContainers.none());
+ if (hasDuplicate(mergedAnnotations, annotationType)) {
+ throw new AnnotationConfigurationException("Found more than one annotation of type " + annotationType
+ + " attributed to " + type
+ + " Please remove the duplicate annotations and publish a bean to handle your authorization logic.");
+ }
+ return AnnotationUtils.findAnnotation(type, annotationType);
+ }
+
+ private static boolean hasDuplicate(MergedAnnotations mergedAnnotations,
+ Class annotationType) {
+ boolean alreadyFound = false;
+ for (MergedAnnotation mergedAnnotation : mergedAnnotations) {
+ if (mergedAnnotation.getType() == annotationType) {
+ if (alreadyFound) {
+ return true;
+ }
+ alreadyFound = true;
+ }
+ }
+ return false;
+ }
+
+ private AuthorizationAnnotationUtils() {
+
+ }
+
+}
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
new file mode 100644
index 00000000000..da6a26bf6e0
--- /dev/null
+++ b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationInterceptorsOrder.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2002-2021 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.aop.Advisor;
+
+/**
+ * Ordering of Spring Security's authorization {@link Advisor}s
+ *
+ * @author Josh Cummings
+ * @since 5.6
+ * @see PreAuthorizeAuthorizationManager
+ * @see PostAuthorizeAuthorizationManager
+ * @see SecuredAuthorizationManager
+ * @see Jsr250AuthorizationManager
+ */
+public enum AuthorizationInterceptorsOrder {
+
+ FIRST(Integer.MIN_VALUE),
+
+ /**
+ * {@link PreFilterAuthorizationMethodInterceptor}
+ */
+ PRE_FILTER,
+
+ PRE_AUTHORIZE,
+
+ SECURED,
+
+ JSR250,
+
+ POST_AUTHORIZE,
+
+ /**
+ * {@link PostFilterAuthorizationMethodInterceptor}
+ */
+ POST_FILTER,
+
+ LAST(Integer.MAX_VALUE);
+
+ private static final int INTERVAL = 100;
+
+ private final int order;
+
+ AuthorizationInterceptorsOrder() {
+ this.order = ordinal() * INTERVAL;
+ }
+
+ AuthorizationInterceptorsOrder(int order) {
+ this.order = order;
+ }
+
+ public int getOrder() {
+ return this.order;
+ }
+
+}
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
new file mode 100644
index 00000000000..cbd43c6270d
--- /dev/null
+++ b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptor.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2002-2021 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.util.function.Supplier;
+
+import org.aopalliance.aop.Advice;
+import org.aopalliance.intercept.MethodInterceptor;
+import org.aopalliance.intercept.MethodInvocation;
+
+import org.springframework.aop.Pointcut;
+import org.springframework.aop.PointcutAdvisor;
+import org.springframework.aop.framework.AopInfrastructureBean;
+import org.springframework.core.Ordered;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.access.prepost.PostAuthorize;
+import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
+import org.springframework.security.authorization.AuthorizationManager;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.util.Assert;
+
+/**
+ * A {@link MethodInterceptor} which can determine if an {@link Authentication} has access
+ * to the result of an {@link MethodInvocation} using an {@link AuthorizationManager}
+ *
+ * @author Evgeniy Cheban
+ * @author Josh Cummings
+ * @since 5.6
+ */
+public final class AuthorizationManagerAfterMethodInterceptor
+ implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
+
+ static final Supplier AUTHENTICATION_SUPPLIER = () -> {
+ Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+ if (authentication == null) {
+ throw new AuthenticationCredentialsNotFoundException(
+ "An Authentication object was not found in the SecurityContext");
+ }
+ return authentication;
+ };
+
+ private final Pointcut pointcut;
+
+ private final AuthorizationManager authorizationManager;
+
+ private int order;
+
+ /**
+ * Creates an instance.
+ * @param pointcut the {@link Pointcut} to use
+ * @param authorizationManager the {@link AuthorizationManager} to use
+ */
+ public AuthorizationManagerAfterMethodInterceptor(Pointcut pointcut,
+ AuthorizationManager authorizationManager) {
+ Assert.notNull(pointcut, "pointcut cannot be null");
+ Assert.notNull(authorizationManager, "authorizationManager cannot be null");
+ this.pointcut = pointcut;
+ this.authorizationManager = authorizationManager;
+ }
+
+ /**
+ * Creates an interceptor for the {@link PostAuthorize} annotation
+ * @return the interceptor
+ */
+ public static AuthorizationManagerAfterMethodInterceptor postAuthorize() {
+ return postAuthorize(new PostAuthorizeAuthorizationManager());
+ }
+
+ /**
+ * Creates an interceptor for the {@link PostAuthorize} annotation
+ * @param authorizationManager the {@link PostAuthorizeAuthorizationManager} to use
+ * @return the interceptor
+ */
+ public static AuthorizationManagerAfterMethodInterceptor postAuthorize(
+ PostAuthorizeAuthorizationManager authorizationManager) {
+ AuthorizationManagerAfterMethodInterceptor interceptor = new AuthorizationManagerAfterMethodInterceptor(
+ AuthorizationMethodPointcuts.forAnnotations(PostAuthorize.class), authorizationManager);
+ interceptor.setOrder(500);
+ return interceptor;
+ }
+
+ /**
+ * Determine if an {@link Authentication} has access to the {@link MethodInvocation}
+ * using the {@link AuthorizationManager}.
+ * @param mi the {@link MethodInvocation} to check
+ * @throws AccessDeniedException if access is not granted
+ */
+ @Override
+ public Object invoke(MethodInvocation mi) throws Throwable {
+ Object result = mi.proceed();
+ this.authorizationManager.verify(AUTHENTICATION_SUPPLIER, new MethodInvocationResult(mi, result));
+ return result;
+ }
+
+ @Override
+ public int getOrder() {
+ return this.order;
+ }
+
+ public void setOrder(int order) {
+ this.order = order;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Pointcut getPointcut() {
+ return this.pointcut;
+ }
+
+ @Override
+ public Advice getAdvice() {
+ return this;
+ }
+
+ @Override
+ public boolean isPerInstance() {
+ return true;
+ }
+
+}
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
new file mode 100644
index 00000000000..0bd1ea05eea
--- /dev/null
+++ b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2002-2021 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.util.function.Supplier;
+
+import javax.annotation.security.DenyAll;
+import javax.annotation.security.PermitAll;
+import javax.annotation.security.RolesAllowed;
+
+import org.aopalliance.aop.Advice;
+import org.aopalliance.intercept.MethodInterceptor;
+import org.aopalliance.intercept.MethodInvocation;
+
+import org.springframework.aop.Pointcut;
+import org.springframework.aop.PointcutAdvisor;
+import org.springframework.aop.framework.AopInfrastructureBean;
+import org.springframework.core.Ordered;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.access.annotation.Secured;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
+import org.springframework.security.authorization.AuthorizationManager;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.util.Assert;
+
+/**
+ * A {@link MethodInterceptor} which uses a {@link AuthorizationManager} to determine if
+ * an {@link Authentication} may invoke the given {@link MethodInvocation}
+ *
+ * @author Evgeniy Cheban
+ * @author Josh Cummings
+ * @since 5.6
+ */
+public final class AuthorizationManagerBeforeMethodInterceptor
+ implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
+
+ static final Supplier AUTHENTICATION_SUPPLIER = () -> {
+ Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+ if (authentication == null) {
+ throw new AuthenticationCredentialsNotFoundException(
+ "An Authentication object was not found in the SecurityContext");
+ }
+ return authentication;
+ };
+
+ private final Pointcut pointcut;
+
+ private final AuthorizationManager authorizationManager;
+
+ private int order = AuthorizationInterceptorsOrder.FIRST.getOrder();
+
+ /**
+ * Creates an instance.
+ * @param pointcut the {@link Pointcut} to use
+ * @param authorizationManager the {@link AuthorizationManager} to use
+ */
+ public AuthorizationManagerBeforeMethodInterceptor(Pointcut pointcut,
+ AuthorizationManager authorizationManager) {
+ Assert.notNull(pointcut, "pointcut cannot be null");
+ Assert.notNull(authorizationManager, "authorizationManager cannot be null");
+ this.pointcut = pointcut;
+ this.authorizationManager = authorizationManager;
+ }
+
+ /**
+ * Creates an interceptor for the {@link PreAuthorize} annotation
+ * @return the interceptor
+ */
+ public static AuthorizationManagerBeforeMethodInterceptor preAuthorize() {
+ return preAuthorize(new PreAuthorizeAuthorizationManager());
+ }
+
+ /**
+ * Creates an interceptor for the {@link PreAuthorize} annotation
+ * @param authorizationManager the {@link PreAuthorizeAuthorizationManager} to use
+ * @return the interceptor
+ */
+ public static AuthorizationManagerBeforeMethodInterceptor preAuthorize(
+ PreAuthorizeAuthorizationManager authorizationManager) {
+ AuthorizationManagerBeforeMethodInterceptor interceptor = new AuthorizationManagerBeforeMethodInterceptor(
+ AuthorizationMethodPointcuts.forAnnotations(PreAuthorize.class), authorizationManager);
+ interceptor.setOrder(AuthorizationInterceptorsOrder.PRE_AUTHORIZE.getOrder());
+ return interceptor;
+ }
+
+ /**
+ * Creates an interceptor for the {@link Secured} annotation
+ * @return the interceptor
+ */
+ public static AuthorizationManagerBeforeMethodInterceptor secured() {
+ return secured(new SecuredAuthorizationManager());
+ }
+
+ /**
+ * Creates an interceptor for the {@link Secured} annotation
+ * @param authorizationManager the {@link SecuredAuthorizationManager} to use
+ * @return the interceptor
+ */
+ public static AuthorizationManagerBeforeMethodInterceptor secured(
+ SecuredAuthorizationManager authorizationManager) {
+ AuthorizationManagerBeforeMethodInterceptor interceptor = new AuthorizationManagerBeforeMethodInterceptor(
+ AuthorizationMethodPointcuts.forAnnotations(Secured.class), authorizationManager);
+ interceptor.setOrder(AuthorizationInterceptorsOrder.SECURED.getOrder());
+ return interceptor;
+ }
+
+ /**
+ * Creates an interceptor for the JSR-250 annotations
+ * @return the interceptor
+ */
+ public static AuthorizationManagerBeforeMethodInterceptor jsr250() {
+ return jsr250(new Jsr250AuthorizationManager());
+ }
+
+ /**
+ * Creates an interceptor for the JSR-250 annotations
+ * @param authorizationManager the {@link Jsr250AuthorizationManager} to use
+ * @return the interceptor
+ */
+ public static AuthorizationManagerBeforeMethodInterceptor jsr250(Jsr250AuthorizationManager authorizationManager) {
+ AuthorizationManagerBeforeMethodInterceptor interceptor = new AuthorizationManagerBeforeMethodInterceptor(
+ AuthorizationMethodPointcuts.forAnnotations(RolesAllowed.class, DenyAll.class, PermitAll.class),
+ authorizationManager);
+ interceptor.setOrder(AuthorizationInterceptorsOrder.JSR250.getOrder());
+ return interceptor;
+ }
+
+ /**
+ * Determine if an {@link Authentication} has access to the {@link MethodInvocation}
+ * using the configured {@link AuthorizationManager}.
+ * @param mi the {@link MethodInvocation} to check
+ * @throws AccessDeniedException if access is not granted
+ */
+ @Override
+ public Object invoke(MethodInvocation mi) throws Throwable {
+ this.authorizationManager.verify(AUTHENTICATION_SUPPLIER, mi);
+ return mi.proceed();
+ }
+
+ @Override
+ public int getOrder() {
+ return this.order;
+ }
+
+ public void setOrder(int order) {
+ this.order = order;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Pointcut getPointcut() {
+ return this.pointcut;
+ }
+
+ @Override
+ public Advice getAdvice() {
+ return this;
+ }
+
+ @Override
+ public boolean isPerInstance() {
+ return true;
+ }
+
+}
diff --git a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationMethodPointcuts.java b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationMethodPointcuts.java
new file mode 100644
index 00000000000..e764d95d838
--- /dev/null
+++ b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationMethodPointcuts.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2002-2021 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.Annotation;
+
+import org.springframework.aop.Pointcut;
+import org.springframework.aop.support.ComposablePointcut;
+import org.springframework.aop.support.Pointcuts;
+import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;
+
+/**
+ * @author Josh Cummings
+ */
+final class AuthorizationMethodPointcuts {
+
+ @SafeVarargs
+ static Pointcut forAnnotations(Class extends Annotation>... annotations) {
+ ComposablePointcut pointcut = null;
+ for (Class extends Annotation> annotation : annotations) {
+ if (pointcut == null) {
+ pointcut = new ComposablePointcut(classOrMethod(annotation));
+ }
+ else {
+ pointcut.union(classOrMethod(annotation));
+ }
+ }
+ return pointcut;
+ }
+
+ private static Pointcut classOrMethod(Class extends Annotation> annotation) {
+ return Pointcuts.union(new AnnotationMatchingPointcut(null, annotation, true),
+ new AnnotationMatchingPointcut(annotation, true));
+ }
+
+ private AuthorizationMethodPointcuts() {
+
+ }
+
+}
diff --git a/core/src/main/java/org/springframework/security/authorization/method/ExpressionAttribute.java b/core/src/main/java/org/springframework/security/authorization/method/ExpressionAttribute.java
new file mode 100644
index 00000000000..80e49360e9b
--- /dev/null
+++ b/core/src/main/java/org/springframework/security/authorization/method/ExpressionAttribute.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2002-2021 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;
+
+/**
+ * An {@link Expression} attribute.
+ *
+ * @author Evgeniy Cheban
+ * @since 5.5
+ */
+class ExpressionAttribute {
+
+ /**
+ * Represents an empty attribute with null {@link Expression}.
+ */
+ static final ExpressionAttribute NULL_ATTRIBUTE = new ExpressionAttribute(null);
+
+ private final Expression expression;
+
+ /**
+ * Creates an instance.
+ * @param expression the {@link Expression} to use
+ */
+ ExpressionAttribute(Expression expression) {
+ this.expression = expression;
+ }
+
+ /**
+ * Returns the {@link Expression}.
+ * @return the {@link Expression} to use
+ */
+ Expression getExpression() {
+ return this.expression;
+ }
+
+}
diff --git a/core/src/main/java/org/springframework/security/authorization/method/Jsr250AuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/method/Jsr250AuthorizationManager.java
new file mode 100644
index 00000000000..ccf8da60410
--- /dev/null
+++ b/core/src/main/java/org/springframework/security/authorization/method/Jsr250AuthorizationManager.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2002-2021 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.Annotation;
+import java.lang.reflect.Method;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.function.Supplier;
+
+import javax.annotation.security.DenyAll;
+import javax.annotation.security.PermitAll;
+import javax.annotation.security.RolesAllowed;
+
+import org.aopalliance.intercept.MethodInvocation;
+
+import org.springframework.aop.support.AopUtils;
+import org.springframework.core.annotation.AnnotationConfigurationException;
+import org.springframework.lang.NonNull;
+import org.springframework.security.authorization.AuthorityAuthorizationManager;
+import org.springframework.security.authorization.AuthorizationDecision;
+import org.springframework.security.authorization.AuthorizationManager;
+import org.springframework.security.core.Authentication;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link AuthorizationManager} which can determine if an {@link Authentication} may
+ * invoke the {@link MethodInvocation} by evaluating if the {@link Authentication}
+ * contains a specified authority from the JSR-250 security annotations.
+ *
+ * @author Evgeniy Cheban
+ * @author Josh Cummings
+ * @since 5.6
+ */
+public final class Jsr250AuthorizationManager implements AuthorizationManager {
+
+ private static final Set> JSR250_ANNOTATIONS = new HashSet<>();
+
+ static {
+ JSR250_ANNOTATIONS.add(DenyAll.class);
+ JSR250_ANNOTATIONS.add(PermitAll.class);
+ JSR250_ANNOTATIONS.add(RolesAllowed.class);
+ }
+
+ private final Jsr250AuthorizationManagerRegistry registry = new Jsr250AuthorizationManagerRegistry();
+
+ private String rolePrefix = "ROLE_";
+
+ /**
+ * Sets the role prefix. Defaults to "ROLE_".
+ * @param rolePrefix the role prefix to use
+ */
+ public void setRolePrefix(String rolePrefix) {
+ Assert.notNull(rolePrefix, "rolePrefix cannot be null");
+ this.rolePrefix = rolePrefix;
+ }
+
+ /**
+ * Determine if an {@link Authentication} has access to a method by evaluating the
+ * {@link DenyAll}, {@link PermitAll}, and {@link RolesAllowed} annotations that
+ * {@link MethodInvocation} specifies.
+ * @param authentication the {@link Supplier} of the {@link Authentication} to check
+ * @param methodInvocation the {@link MethodInvocation} to check
+ * @return an {@link AuthorizationDecision} or null if the JSR-250 security
+ * annotations is not present
+ */
+ @Override
+ public AuthorizationDecision check(Supplier authentication, MethodInvocation methodInvocation) {
+ AuthorizationManager delegate = this.registry.getManager(methodInvocation);
+ return delegate.check(authentication, methodInvocation);
+ }
+
+ private final class Jsr250AuthorizationManagerRegistry extends AbstractAuthorizationManagerRegistry {
+
+ @NonNull
+ @Override
+ AuthorizationManager resolveManager(Method method, Class> targetClass) {
+ Annotation annotation = findJsr250Annotation(method, targetClass);
+ if (annotation instanceof DenyAll) {
+ return (a, o) -> new AuthorizationDecision(false);
+ }
+ if (annotation instanceof PermitAll) {
+ return (a, o) -> new AuthorizationDecision(true);
+ }
+ if (annotation instanceof RolesAllowed) {
+ RolesAllowed rolesAllowed = (RolesAllowed) annotation;
+ return AuthorityAuthorizationManager.hasAnyRole(Jsr250AuthorizationManager.this.rolePrefix,
+ rolesAllowed.value());
+ }
+ return NULL_MANAGER;
+ }
+
+ private Annotation findJsr250Annotation(Method method, Class> targetClass) {
+ Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
+ Annotation annotation = findAnnotation(specificMethod);
+ return (annotation != null) ? annotation : findAnnotation(specificMethod.getDeclaringClass());
+ }
+
+ private Annotation findAnnotation(Method method) {
+ Set annotations = new HashSet<>();
+ for (Class extends Annotation> annotationClass : JSR250_ANNOTATIONS) {
+ Annotation annotation = AuthorizationAnnotationUtils.findUniqueAnnotation(method, annotationClass);
+ if (annotation != null) {
+ annotations.add(annotation);
+ }
+ }
+ if (annotations.isEmpty()) {
+ return null;
+ }
+ if (annotations.size() > 1) {
+ throw new AnnotationConfigurationException(
+ "The JSR-250 specification disallows DenyAll, PermitAll, and RolesAllowed from appearing on the same method.");
+ }
+ return annotations.iterator().next();
+ }
+
+ private Annotation findAnnotation(Class> clazz) {
+ Set annotations = new HashSet<>();
+ for (Class extends Annotation> annotationClass : JSR250_ANNOTATIONS) {
+ Annotation annotation = AuthorizationAnnotationUtils.findUniqueAnnotation(clazz, annotationClass);
+ if (annotation != null) {
+ annotations.add(annotation);
+ }
+ }
+ if (annotations.isEmpty()) {
+ return null;
+ }
+ if (annotations.size() > 1) {
+ throw new AnnotationConfigurationException(
+ "The JSR-250 specification disallows DenyAll, PermitAll, and RolesAllowed from appearing on the same class definition.");
+ }
+ return annotations.iterator().next();
+ }
+
+ }
+
+}
diff --git a/core/src/main/java/org/springframework/security/authorization/method/MethodInvocationResult.java b/core/src/main/java/org/springframework/security/authorization/method/MethodInvocationResult.java
new file mode 100644
index 00000000000..35250f77df9
--- /dev/null
+++ b/core/src/main/java/org/springframework/security/authorization/method/MethodInvocationResult.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2002-2021 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.util.Assert;
+
+/**
+ * A context object that contains a {@link MethodInvocation} and the result of that
+ * {@link MethodInvocation}.
+ *
+ * @author Josh Cummings
+ * @since 5.6
+ */
+public class MethodInvocationResult {
+
+ private final MethodInvocation methodInvocation;
+
+ private final Object result;
+
+ /**
+ * Construct a {@link MethodInvocationResult} with the provided parameters
+ * @param methodInvocation the already-invoked {@link MethodInvocation}
+ * @param result the value returned from the {@link MethodInvocation}
+ */
+ public MethodInvocationResult(MethodInvocation methodInvocation, Object result) {
+ Assert.notNull(methodInvocation, "methodInvocation cannot be null");
+ this.methodInvocation = methodInvocation;
+ this.result = result;
+ }
+
+ /**
+ * Return the already-invoked {@link MethodInvocation}
+ * @return the already-invoked {@link MethodInvocation}
+ */
+ public MethodInvocation getMethodInvocation() {
+ return this.methodInvocation;
+ }
+
+ /**
+ * Return the result of the already-invoked {@link MethodInvocation}
+ * @return the result
+ */
+ public Object getResult() {
+ return this.result;
+ }
+
+}
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
new file mode 100644
index 00000000000..69886476311
--- /dev/null
+++ b/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2002-2021 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.function.Supplier;
+
+import org.aopalliance.intercept.MethodInvocation;
+import reactor.util.annotation.NonNull;
+
+import org.springframework.aop.support.AopUtils;
+import org.springframework.expression.EvaluationContext;
+import org.springframework.expression.Expression;
+import org.springframework.security.access.expression.ExpressionUtils;
+import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
+import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
+import org.springframework.security.access.prepost.PostAuthorize;
+import org.springframework.security.authorization.AuthorizationDecision;
+import org.springframework.security.authorization.AuthorizationManager;
+import org.springframework.security.core.Authentication;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link AuthorizationManager} which can determine if an {@link Authentication} may
+ * return the result from an invoked {@link MethodInvocation} by evaluating an expression
+ * from the {@link PostAuthorize} annotation.
+ *
+ * @author Evgeniy Cheban
+ * @since 5.6
+ */
+public final class PostAuthorizeAuthorizationManager implements AuthorizationManager {
+
+ private final PostAuthorizeExpressionAttributeRegistry registry = new PostAuthorizeExpressionAttributeRegistry();
+
+ private MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
+
+ /**
+ * Use this the {@link MethodSecurityExpressionHandler}.
+ * @param expressionHandler the {@link MethodSecurityExpressionHandler} to use
+ */
+ public void setExpressionHandler(MethodSecurityExpressionHandler expressionHandler) {
+ Assert.notNull(expressionHandler, "expressionHandler cannot be null");
+ this.expressionHandler = expressionHandler;
+ }
+
+ /**
+ * Determine if an {@link Authentication} has access to the returned object by
+ * evaluating the {@link PostAuthorize} annotation that the {@link MethodInvocation}
+ * specifies.
+ * @param authentication the {@link Supplier} of the {@link Authentication} to check
+ * @param mi the {@link MethodInvocationResult} to check
+ * @return an {@link AuthorizationDecision} or {@code null} if the
+ * {@link PostAuthorize} annotation is not present
+ */
+ @Override
+ public AuthorizationDecision check(Supplier authentication, MethodInvocationResult mi) {
+ ExpressionAttribute attribute = this.registry.getAttribute(mi.getMethodInvocation());
+ if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) {
+ return null;
+ }
+ EvaluationContext ctx = this.expressionHandler.createEvaluationContext(authentication.get(),
+ mi.getMethodInvocation());
+ this.expressionHandler.setReturnObject(mi.getResult(), ctx);
+ boolean granted = ExpressionUtils.evaluateAsBoolean(attribute.getExpression(), ctx);
+ return new AuthorizationDecision(granted);
+ }
+
+ private final class PostAuthorizeExpressionAttributeRegistry
+ extends AbstractExpressionAttributeRegistry {
+
+ @NonNull
+ @Override
+ ExpressionAttribute resolveAttribute(Method method, Class> targetClass) {
+ Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
+ PostAuthorize postAuthorize = findPostAuthorizeAnnotation(specificMethod);
+ if (postAuthorize == null) {
+ return ExpressionAttribute.NULL_ATTRIBUTE;
+ }
+ Expression postAuthorizeExpression = PostAuthorizeAuthorizationManager.this.expressionHandler
+ .getExpressionParser().parseExpression(postAuthorize.value());
+ return new ExpressionAttribute(postAuthorizeExpression);
+ }
+
+ private PostAuthorize findPostAuthorizeAnnotation(Method method) {
+ PostAuthorize postAuthorize = AuthorizationAnnotationUtils.findUniqueAnnotation(method,
+ PostAuthorize.class);
+ return (postAuthorize != null) ? postAuthorize : AuthorizationAnnotationUtils
+ .findUniqueAnnotation(method.getDeclaringClass(), PostAuthorize.class);
+ }
+
+ }
+
+}
diff --git a/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptor.java
new file mode 100644
index 00000000000..737d7aa8e26
--- /dev/null
+++ b/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptor.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2002-2021 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.function.Supplier;
+
+import org.aopalliance.aop.Advice;
+import org.aopalliance.intercept.MethodInterceptor;
+import org.aopalliance.intercept.MethodInvocation;
+
+import org.springframework.aop.Pointcut;
+import org.springframework.aop.PointcutAdvisor;
+import org.springframework.aop.framework.AopInfrastructureBean;
+import org.springframework.aop.support.AopUtils;
+import org.springframework.core.Ordered;
+import org.springframework.expression.EvaluationContext;
+import org.springframework.expression.Expression;
+import org.springframework.lang.NonNull;
+import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
+import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
+import org.springframework.security.access.prepost.PostFilter;
+import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.util.Assert;
+
+/**
+ * A {@link MethodInterceptor} which filters a {@code returnedObject} from the
+ * {@link MethodInvocation} by evaluating an expression from the {@link PostFilter}
+ * annotation.
+ *
+ * @author Evgeniy Cheban
+ * @author Josh Cummings
+ * @since 5.6
+ */
+public final class PostFilterAuthorizationMethodInterceptor
+ implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
+
+ private static final Supplier AUTHENTICATION_SUPPLIER = () -> {
+ Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+ if (authentication == null) {
+ throw new AuthenticationCredentialsNotFoundException(
+ "An Authentication object was not found in the SecurityContext");
+ }
+ return authentication;
+ };
+
+ private final PostFilterExpressionAttributeRegistry registry = new PostFilterExpressionAttributeRegistry();
+
+ private int order = AuthorizationInterceptorsOrder.POST_FILTER.getOrder();
+
+ private final Pointcut pointcut;
+
+ private MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
+
+ /**
+ * Creates a {@link PostFilterAuthorizationMethodInterceptor} using the provided
+ * parameters
+ */
+ public PostFilterAuthorizationMethodInterceptor() {
+ this.pointcut = AuthorizationMethodPointcuts.forAnnotations(PostFilter.class);
+ }
+
+ /**
+ * Use this {@link MethodSecurityExpressionHandler}.
+ * @param expressionHandler the {@link MethodSecurityExpressionHandler} to use
+ */
+ public void setExpressionHandler(MethodSecurityExpressionHandler expressionHandler) {
+ Assert.notNull(expressionHandler, "expressionHandler cannot be null");
+ this.expressionHandler = expressionHandler;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getOrder() {
+ return this.order;
+ }
+
+ public void setOrder(int order) {
+ this.order = order;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Pointcut getPointcut() {
+ return this.pointcut;
+ }
+
+ @Override
+ public Advice getAdvice() {
+ return this;
+ }
+
+ @Override
+ public boolean isPerInstance() {
+ return true;
+ }
+
+ /**
+ * Filter a {@code returnedObject} using the {@link PostFilter} annotation that the
+ * {@link MethodInvocation} specifies.
+ * @param mi the {@link MethodInvocation} to check check
+ * @return filtered {@code returnedObject}
+ */
+ @Override
+ public Object invoke(MethodInvocation mi) throws Throwable {
+ Object returnedObject = mi.proceed();
+ ExpressionAttribute attribute = this.registry.getAttribute(mi);
+ if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) {
+ return returnedObject;
+ }
+ EvaluationContext ctx = this.expressionHandler.createEvaluationContext(AUTHENTICATION_SUPPLIER.get(), mi);
+ return this.expressionHandler.filter(returnedObject, attribute.getExpression(), ctx);
+ }
+
+ private final class PostFilterExpressionAttributeRegistry
+ extends AbstractExpressionAttributeRegistry {
+
+ @NonNull
+ @Override
+ ExpressionAttribute resolveAttribute(Method method, Class> targetClass) {
+ Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
+ PostFilter postFilter = findPostFilterAnnotation(specificMethod);
+ if (postFilter == null) {
+ return ExpressionAttribute.NULL_ATTRIBUTE;
+ }
+ Expression postFilterExpression = PostFilterAuthorizationMethodInterceptor.this.expressionHandler
+ .getExpressionParser().parseExpression(postFilter.value());
+ return new ExpressionAttribute(postFilterExpression);
+ }
+
+ private PostFilter findPostFilterAnnotation(Method method) {
+ PostFilter postFilter = AuthorizationAnnotationUtils.findUniqueAnnotation(method, PostFilter.class);
+ return (postFilter != null) ? postFilter
+ : AuthorizationAnnotationUtils.findUniqueAnnotation(method.getDeclaringClass(), PostFilter.class);
+ }
+
+ }
+
+}
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
new file mode 100644
index 00000000000..c7f0f9523df
--- /dev/null
+++ b/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2002-2021 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.function.Supplier;
+
+import org.aopalliance.intercept.MethodInvocation;
+import reactor.util.annotation.NonNull;
+
+import org.springframework.aop.support.AopUtils;
+import org.springframework.expression.EvaluationContext;
+import org.springframework.expression.Expression;
+import org.springframework.security.access.expression.ExpressionUtils;
+import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
+import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.authorization.AuthorizationDecision;
+import org.springframework.security.authorization.AuthorizationManager;
+import org.springframework.security.core.Authentication;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link AuthorizationManager} which can determine if an {@link Authentication} may
+ * invoke the {@link MethodInvocation} by evaluating an expression from the
+ * {@link PreAuthorize} annotation.
+ *
+ * @author Evgeniy Cheban
+ * @since 5.6
+ */
+public final class PreAuthorizeAuthorizationManager implements AuthorizationManager {
+
+ private final PreAuthorizeExpressionAttributeRegistry registry = new PreAuthorizeExpressionAttributeRegistry();
+
+ private MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
+
+ /**
+ * Sets the {@link MethodSecurityExpressionHandler}.
+ * @param expressionHandler the {@link MethodSecurityExpressionHandler} to use
+ */
+ public void setExpressionHandler(MethodSecurityExpressionHandler expressionHandler) {
+ Assert.notNull(expressionHandler, "expressionHandler cannot be null");
+ this.expressionHandler = expressionHandler;
+ }
+
+ /**
+ * Determine if an {@link Authentication} has access to a method by evaluating an
+ * expression from the {@link PreAuthorize} annotation that the
+ * {@link MethodInvocation} specifies.
+ * @param authentication the {@link Supplier} of the {@link Authentication} to check
+ * @param mi the {@link MethodInvocation} to check
+ * @return an {@link AuthorizationDecision} or {@code null} if the
+ * {@link PreAuthorize} annotation is not present
+ */
+ @Override
+ public AuthorizationDecision check(Supplier authentication, MethodInvocation mi) {
+ ExpressionAttribute attribute = this.registry.getAttribute(mi);
+ if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) {
+ return null;
+ }
+ EvaluationContext ctx = this.expressionHandler.createEvaluationContext(authentication.get(), mi);
+ boolean granted = ExpressionUtils.evaluateAsBoolean(attribute.getExpression(), ctx);
+ return new AuthorizationDecision(granted);
+ }
+
+ private final class PreAuthorizeExpressionAttributeRegistry
+ extends AbstractExpressionAttributeRegistry {
+
+ @NonNull
+ @Override
+ ExpressionAttribute resolveAttribute(Method method, Class> targetClass) {
+ Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
+ PreAuthorize preAuthorize = findPreAuthorizeAnnotation(specificMethod);
+ if (preAuthorize == null) {
+ return ExpressionAttribute.NULL_ATTRIBUTE;
+ }
+ Expression preAuthorizeExpression = PreAuthorizeAuthorizationManager.this.expressionHandler
+ .getExpressionParser().parseExpression(preAuthorize.value());
+ return new ExpressionAttribute(preAuthorizeExpression);
+ }
+
+ private PreAuthorize findPreAuthorizeAnnotation(Method method) {
+ PreAuthorize preAuthorize = AuthorizationAnnotationUtils.findUniqueAnnotation(method, PreAuthorize.class);
+ return (preAuthorize != null) ? preAuthorize
+ : AuthorizationAnnotationUtils.findUniqueAnnotation(method.getDeclaringClass(), PreAuthorize.class);
+ }
+
+ }
+
+}
diff --git a/core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptor.java
new file mode 100644
index 00000000000..d9cfc29dee5
--- /dev/null
+++ b/core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptor.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2002-2021 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.function.Supplier;
+
+import org.aopalliance.aop.Advice;
+import org.aopalliance.intercept.MethodInterceptor;
+import org.aopalliance.intercept.MethodInvocation;
+
+import org.springframework.aop.Pointcut;
+import org.springframework.aop.PointcutAdvisor;
+import org.springframework.aop.framework.AopInfrastructureBean;
+import org.springframework.aop.support.AopUtils;
+import org.springframework.core.Ordered;
+import org.springframework.expression.EvaluationContext;
+import org.springframework.expression.Expression;
+import org.springframework.lang.NonNull;
+import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
+import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
+import org.springframework.security.access.prepost.PreFilter;
+import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * A {@link MethodInterceptor} which filters a method argument by evaluating an expression
+ * from the {@link PreFilter} annotation.
+ *
+ * @author Evgeniy Cheban
+ * @author Josh Cummings
+ * @since 5.6
+ */
+public final class PreFilterAuthorizationMethodInterceptor
+ implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
+
+ private static final Supplier AUTHENTICATION_SUPPLIER = () -> {
+ Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+ if (authentication == null) {
+ throw new AuthenticationCredentialsNotFoundException(
+ "An Authentication object was not found in the SecurityContext");
+ }
+ return authentication;
+ };
+
+ private final PreFilterExpressionAttributeRegistry registry = new PreFilterExpressionAttributeRegistry();
+
+ private int order = AuthorizationInterceptorsOrder.PRE_FILTER.getOrder();
+
+ private final Pointcut pointcut;
+
+ private MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
+
+ /**
+ * Creates a {@link PreFilterAuthorizationMethodInterceptor} using the provided
+ * parameters
+ */
+ public PreFilterAuthorizationMethodInterceptor() {
+ this.pointcut = AuthorizationMethodPointcuts.forAnnotations(PreFilter.class);
+ }
+
+ /**
+ * Use this {@link MethodSecurityExpressionHandler}
+ * @param expressionHandler the {@link MethodSecurityExpressionHandler} to use
+ */
+ public void setExpressionHandler(MethodSecurityExpressionHandler expressionHandler) {
+ Assert.notNull(expressionHandler, "expressionHandler cannot be null");
+ this.expressionHandler = expressionHandler;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getOrder() {
+ return this.order;
+ }
+
+ public void setOrder(int order) {
+ this.order = order;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Pointcut getPointcut() {
+ return this.pointcut;
+ }
+
+ @Override
+ public Advice getAdvice() {
+ return this;
+ }
+
+ @Override
+ public boolean isPerInstance() {
+ return true;
+ }
+
+ /**
+ * Filter the method argument specified in the {@link PreFilter} annotation that
+ * {@link MethodInvocation} specifies.
+ * @param mi the {@link MethodInvocation} to check
+ */
+ @Override
+ public Object invoke(MethodInvocation mi) throws Throwable {
+ PreFilterExpressionAttribute attribute = this.registry.getAttribute(mi);
+ if (attribute == PreFilterExpressionAttribute.NULL_ATTRIBUTE) {
+ return mi.proceed();
+ }
+ EvaluationContext ctx = this.expressionHandler.createEvaluationContext(AUTHENTICATION_SUPPLIER.get(), mi);
+ Object filterTarget = findFilterTarget(attribute.filterTarget, ctx, mi);
+ this.expressionHandler.filter(filterTarget, attribute.getExpression(), ctx);
+ return mi.proceed();
+ }
+
+ private Object findFilterTarget(String filterTargetName, EvaluationContext ctx, MethodInvocation methodInvocation) {
+ Object filterTarget;
+ if (StringUtils.hasText(filterTargetName)) {
+ filterTarget = ctx.lookupVariable(filterTargetName);
+ Assert.notNull(filterTarget, () -> "Filter target was null, or no argument with name '" + filterTargetName
+ + "' found in method.");
+ }
+ else {
+ Object[] arguments = methodInvocation.getArguments();
+ Assert.state(arguments.length == 1,
+ "Unable to determine the method argument for filtering. Specify the filter target.");
+ filterTarget = arguments[0];
+ Assert.notNull(filterTarget,
+ "Filter target was null. Make sure you passing the correct value in the method argument.");
+ }
+ Assert.state(!filterTarget.getClass().isArray(),
+ "Pre-filtering on array types is not supported. Using a Collection will solve this problem.");
+ return filterTarget;
+ }
+
+ private final class PreFilterExpressionAttributeRegistry
+ extends AbstractExpressionAttributeRegistry {
+
+ @NonNull
+ @Override
+ PreFilterExpressionAttribute resolveAttribute(Method method, Class> targetClass) {
+ Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
+ PreFilter preFilter = findPreFilterAnnotation(specificMethod);
+ if (preFilter == null) {
+ return PreFilterExpressionAttribute.NULL_ATTRIBUTE;
+ }
+ Expression preFilterExpression = PreFilterAuthorizationMethodInterceptor.this.expressionHandler
+ .getExpressionParser().parseExpression(preFilter.value());
+ return new PreFilterExpressionAttribute(preFilterExpression, preFilter.filterTarget());
+ }
+
+ private PreFilter findPreFilterAnnotation(Method method) {
+ PreFilter preFilter = AuthorizationAnnotationUtils.findUniqueAnnotation(method, PreFilter.class);
+ return (preFilter != null) ? preFilter
+ : AuthorizationAnnotationUtils.findUniqueAnnotation(method.getDeclaringClass(), PreFilter.class);
+ }
+
+ }
+
+ private static final class PreFilterExpressionAttribute extends ExpressionAttribute {
+
+ private static final PreFilterExpressionAttribute NULL_ATTRIBUTE = new PreFilterExpressionAttribute(null, null);
+
+ private final String filterTarget;
+
+ private PreFilterExpressionAttribute(Expression expression, String filterTarget) {
+ super(expression);
+ this.filterTarget = filterTarget;
+ }
+
+ }
+
+}
diff --git a/core/src/main/java/org/springframework/security/authorization/method/SecuredAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/method/SecuredAuthorizationManager.java
new file mode 100644
index 00000000000..73ad02ce38b
--- /dev/null
+++ b/core/src/main/java/org/springframework/security/authorization/method/SecuredAuthorizationManager.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2002-2021 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.function.Supplier;
+
+import org.aopalliance.intercept.MethodInvocation;
+
+import org.springframework.aop.support.AopUtils;
+import org.springframework.lang.NonNull;
+import org.springframework.security.access.annotation.Secured;
+import org.springframework.security.authorization.AuthorityAuthorizationManager;
+import org.springframework.security.authorization.AuthorizationDecision;
+import org.springframework.security.authorization.AuthorizationManager;
+import org.springframework.security.core.Authentication;
+
+/**
+ * An {@link AuthorizationManager} which can determine if an {@link Authentication} may
+ * invoke the {@link MethodInvocation} by evaluating if the {@link Authentication}
+ * contains a specified authority from the Spring Security's {@link Secured} annotation.
+ *
+ * @author Evgeniy Cheban
+ * @since 5.6
+ */
+public final class SecuredAuthorizationManager implements AuthorizationManager {
+
+ private final SecuredAuthorizationManagerRegistry registry = new SecuredAuthorizationManagerRegistry();
+
+ /**
+ * Determine if an {@link Authentication} has access to a method by evaluating the
+ * {@link Secured} annotation that {@link MethodInvocation} specifies.
+ * @param authentication the {@link Supplier} of the {@link Authentication} to check
+ * @param mi the {@link MethodInvocation} to check
+ * @return an {@link AuthorizationDecision} or null if the {@link Secured} annotation
+ * is not present
+ */
+ @Override
+ public AuthorizationDecision check(Supplier authentication, MethodInvocation mi) {
+ AuthorizationManager delegate = this.registry.getManager(mi);
+ return delegate.check(authentication, mi);
+ }
+
+ private static final class SecuredAuthorizationManagerRegistry extends AbstractAuthorizationManagerRegistry {
+
+ @NonNull
+ @Override
+ AuthorizationManager resolveManager(Method method, Class> targetClass) {
+ Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
+ Secured secured = findSecuredAnnotation(specificMethod);
+ return (secured != null) ? AuthorityAuthorizationManager.hasAnyAuthority(secured.value()) : NULL_MANAGER;
+ }
+
+ private Secured findSecuredAnnotation(Method method) {
+ Secured secured = AuthorizationAnnotationUtils.findUniqueAnnotation(method, Secured.class);
+ return (secured != null) ? secured
+ : AuthorizationAnnotationUtils.findUniqueAnnotation(method.getDeclaringClass(), Secured.class);
+ }
+
+ }
+
+}
diff --git a/core/src/test/java/org/springframework/security/access/annotation/BusinessService.java b/core/src/test/java/org/springframework/security/access/annotation/BusinessService.java
index 16c63b7e28f..7d42e18f3c0 100644
--- a/core/src/test/java/org/springframework/security/access/annotation/BusinessService.java
+++ b/core/src/test/java/org/springframework/security/access/annotation/BusinessService.java
@@ -60,4 +60,10 @@ public interface BusinessService extends Serializable {
List> methodReturningAList(String userName, String extraParam);
+ @RequireAdminRole
+ @RequireUserRole
+ default void repeatedAnnotations() {
+
+ }
+
}
diff --git a/core/src/test/java/org/springframework/security/access/annotation/RequireAdminRole.java b/core/src/test/java/org/springframework/security/access/annotation/RequireAdminRole.java
new file mode 100644
index 00000000000..70e68eda6ea
--- /dev/null
+++ b/core/src/test/java/org/springframework/security/access/annotation/RequireAdminRole.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2002-2021 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.access.annotation;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import javax.annotation.security.RolesAllowed;
+
+import org.springframework.security.access.prepost.PreAuthorize;
+
+@Retention(RetentionPolicy.RUNTIME)
+@PreAuthorize("hasRole('ADMIN')")
+@RolesAllowed("ADMIN")
+@Secured("ADMIN")
+public @interface RequireAdminRole {
+
+}
diff --git a/core/src/test/java/org/springframework/security/access/annotation/RequireUserRole.java b/core/src/test/java/org/springframework/security/access/annotation/RequireUserRole.java
new file mode 100644
index 00000000000..a11b7fc6def
--- /dev/null
+++ b/core/src/test/java/org/springframework/security/access/annotation/RequireUserRole.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2002-2021 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.access.annotation;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import javax.annotation.security.RolesAllowed;
+
+import org.springframework.security.access.prepost.PreAuthorize;
+
+@Retention(RetentionPolicy.RUNTIME)
+@PreAuthorize("hasRole('USER')")
+@RolesAllowed("ADMIN")
+@Secured("USER")
+public @interface RequireUserRole {
+
+}
diff --git a/core/src/test/java/org/springframework/security/authorization/AuthorityAuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/AuthorityAuthorizationManagerTests.java
index ab0c41563c7..5c092d54d43 100644
--- a/core/src/test/java/org/springframework/security/authorization/AuthorityAuthorizationManagerTests.java
+++ b/core/src/test/java/org/springframework/security/authorization/AuthorityAuthorizationManagerTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2020 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -64,6 +64,13 @@ public void hasAnyRoleWhenContainNullThenException() {
.withMessage("roles cannot contain null values");
}
+ @Test
+ public void hasAnyRoleWhenCustomRolePrefixNullThenException() {
+ assertThatIllegalArgumentException()
+ .isThrownBy(() -> AuthorityAuthorizationManager.hasAnyRole(null, new String[] { "ADMIN", "USER" }))
+ .withMessage("rolePrefix cannot be null");
+ }
+
@Test
public void hasAnyAuthorityWhenNullThenException() {
assertThatIllegalArgumentException().isThrownBy(() -> AuthorityAuthorizationManager.hasAnyAuthority(null))
@@ -147,6 +154,17 @@ public void hasAnyRoleWhenUserHasNotAnyRoleThenDeniedDecision() {
assertThat(manager.check(authentication, object).isGranted()).isFalse();
}
+ @Test
+ public void hasAnyRoleWhenCustomRolePrefixProvidedThenUseCustomRolePrefix() {
+ AuthorityAuthorizationManager manager = AuthorityAuthorizationManager.hasAnyRole("CUSTOM_",
+ new String[] { "USER" });
+ Supplier authentication = () -> new TestingAuthenticationToken("user", "password",
+ "CUSTOM_USER");
+ Object object = new Object();
+
+ assertThat(manager.check(authentication, object).isGranted()).isTrue();
+ }
+
@Test
public void hasAnyAuthorityWhenUserHasAnyAuthorityThenGrantedDecision() {
AuthorityAuthorizationManager manager = AuthorityAuthorizationManager.hasAnyAuthority("ADMIN", "USER");
diff --git a/core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptorTests.java b/core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptorTests.java
new file mode 100644
index 00000000000..26287ed97bb
--- /dev/null
+++ b/core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptorTests.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2002-2021 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.junit.Test;
+
+import org.springframework.aop.Pointcut;
+import org.springframework.security.authorization.AuthorizationManager;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Tests for {@link AuthorizationManagerAfterMethodInterceptor}.
+ *
+ * @author Evgeniy Cheban
+ */
+public class AuthorizationManagerAfterMethodInterceptorTests {
+
+ @Test
+ public void instantiateWhenMethodMatcherNullThenException() {
+ AuthorizationManager mockAuthorizationManager = mock(AuthorizationManager.class);
+ assertThatIllegalArgumentException()
+ .isThrownBy(() -> new AuthorizationManagerAfterMethodInterceptor(null, mockAuthorizationManager))
+ .withMessage("pointcut cannot be null");
+ }
+
+ @Test
+ public void instantiateWhenAuthorizationManagerNullThenException() {
+ assertThatIllegalArgumentException()
+ .isThrownBy(() -> new AuthorizationManagerAfterMethodInterceptor(mock(Pointcut.class), null))
+ .withMessage("authorizationManager cannot be null");
+ }
+
+ @Test
+ public void beforeWhenMockAuthorizationManagerThenVerifyAndReturnedObject() throws Throwable {
+ MethodInvocation mockMethodInvocation = mock(MethodInvocation.class);
+ MethodInvocationResult result = new MethodInvocationResult(mockMethodInvocation, new Object());
+ given(mockMethodInvocation.proceed()).willReturn(result.getResult());
+ AuthorizationManager mockAuthorizationManager = mock(AuthorizationManager.class);
+ AuthorizationManagerAfterMethodInterceptor advice = new AuthorizationManagerAfterMethodInterceptor(
+ Pointcut.TRUE, mockAuthorizationManager);
+ Object returnedObject = advice.invoke(mockMethodInvocation);
+ assertThat(returnedObject).isEqualTo(result.getResult());
+ verify(mockAuthorizationManager).verify(eq(AuthorizationManagerAfterMethodInterceptor.AUTHENTICATION_SUPPLIER),
+ any(MethodInvocationResult.class));
+ }
+
+}
diff --git a/core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptorTests.java b/core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptorTests.java
new file mode 100644
index 00000000000..01456bd40ca
--- /dev/null
+++ b/core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptorTests.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2002-2021 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.junit.Test;
+
+import org.springframework.aop.Pointcut;
+import org.springframework.security.authorization.AuthorizationManager;
+
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Tests for {@link AuthorizationManagerBeforeMethodInterceptor}.
+ *
+ * @author Evgeniy Cheban
+ */
+public class AuthorizationManagerBeforeMethodInterceptorTests {
+
+ @Test
+ public void instantiateWhenMethodMatcherNullThenException() {
+ assertThatIllegalArgumentException()
+ .isThrownBy(
+ () -> new AuthorizationManagerBeforeMethodInterceptor(null, mock(AuthorizationManager.class)))
+ .withMessage("pointcut cannot be null");
+ }
+
+ @Test
+ public void instantiateWhenAuthorizationManagerNullThenException() {
+ assertThatIllegalArgumentException()
+ .isThrownBy(() -> new AuthorizationManagerBeforeMethodInterceptor(mock(Pointcut.class), null))
+ .withMessage("authorizationManager cannot be null");
+ }
+
+ @Test
+ public void beforeWhenMockAuthorizationManagerThenVerify() throws Throwable {
+ MethodInvocation mockMethodInvocation = mock(MethodInvocation.class);
+ AuthorizationManager mockAuthorizationManager = mock(AuthorizationManager.class);
+ AuthorizationManagerBeforeMethodInterceptor advice = new AuthorizationManagerBeforeMethodInterceptor(
+ Pointcut.TRUE, mockAuthorizationManager);
+ advice.invoke(mockMethodInvocation);
+ verify(mockAuthorizationManager).verify(AuthorizationManagerBeforeMethodInterceptor.AUTHENTICATION_SUPPLIER,
+ mockMethodInvocation);
+ }
+
+}
diff --git a/core/src/test/java/org/springframework/security/authorization/method/AuthorizationMethodPointcutsTests.java b/core/src/test/java/org/springframework/security/authorization/method/AuthorizationMethodPointcutsTests.java
new file mode 100644
index 00000000000..b2bb8de7b38
--- /dev/null
+++ b/core/src/test/java/org/springframework/security/authorization/method/AuthorizationMethodPointcutsTests.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2002-2021 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;
+
+import org.junit.Test;
+
+import org.springframework.aop.Pointcut;
+import org.springframework.aop.support.AopUtils;
+import org.springframework.security.access.prepost.PreAuthorize;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link AuthorizationMethodPointcuts}
+ */
+public class AuthorizationMethodPointcutsTests {
+
+ @Test
+ public void forAnnotationsWhenAnnotationThenClassBasedAnnotationPointcut() {
+ Pointcut preAuthorize = AuthorizationMethodPointcuts.forAnnotations(PreAuthorize.class);
+ assertThat(AopUtils.canApply(preAuthorize, ClassController.class)).isTrue();
+ assertThat(AopUtils.canApply(preAuthorize, NoController.class)).isFalse();
+ }
+
+ @Test
+ public void forAnnotationsWhenAnnotationThenMethodBasedAnnotationPointcut() {
+ Pointcut preAuthorize = AuthorizationMethodPointcuts.forAnnotations(PreAuthorize.class);
+ assertThat(AopUtils.canApply(preAuthorize, MethodController.class)).isTrue();
+ }
+
+ @Test
+ public void forAnnotationsWhenAnnotationThenClassInheritancePointcut() {
+ Pointcut preAuthorize = AuthorizationMethodPointcuts.forAnnotations(PreAuthorize.class);
+ assertThat(AopUtils.canApply(preAuthorize, InterfacedClassController.class)).isTrue();
+ }
+
+ @Test
+ public void forAnnotationsWhenAnnotationThenMethodInheritancePointcut() {
+ Pointcut preAuthorize = AuthorizationMethodPointcuts.forAnnotations(PreAuthorize.class);
+ assertThat(AopUtils.canApply(preAuthorize, InterfacedMethodController.class)).isTrue();
+ }
+
+ @Test
+ public void forAnnotationsWhenAnnotationThenAnnotationClassInheritancePointcut() {
+ Pointcut preAuthorize = AuthorizationMethodPointcuts.forAnnotations(PreAuthorize.class);
+ assertThat(AopUtils.canApply(preAuthorize, InterfacedAnnotationClassController.class)).isTrue();
+ }
+
+ @Test
+ public void forAnnotationsWhenAnnotationThenAnnotationMethodInheritancePointcut() {
+ Pointcut preAuthorize = AuthorizationMethodPointcuts.forAnnotations(PreAuthorize.class);
+ assertThat(AopUtils.canApply(preAuthorize, InterfacedAnnotationMethodController.class)).isTrue();
+ }
+
+ @PreAuthorize("hasAuthority('APP')")
+ public static class ClassController {
+
+ String methodOne(String paramOne) {
+ return "value";
+ }
+
+ }
+
+ public static class MethodController {
+
+ @PreAuthorize("hasAuthority('APP')")
+ String methodOne(String paramOne) {
+ return "value";
+ }
+
+ }
+
+ public static class NoController {
+
+ String methodOne(String paramOne) {
+ return "value";
+ }
+
+ }
+
+ @PreAuthorize("hasAuthority('APP')")
+ public interface ClassControllerInterface {
+
+ String methodOne(String paramOne);
+
+ }
+
+ public static class InterfacedClassController implements ClassControllerInterface {
+
+ public String methodOne(String paramOne) {
+ return "value";
+ }
+
+ }
+
+ public interface MethodControllerInterface {
+
+ @PreAuthorize("hasAuthority('APP')")
+ String methodOne(String paramOne);
+
+ }
+
+ public static class InterfacedMethodController implements MethodControllerInterface {
+
+ public String methodOne(String paramOne) {
+ return "value";
+ }
+
+ }
+
+ @Target({ ElementType.METHOD, ElementType.TYPE })
+ @Retention(RetentionPolicy.RUNTIME)
+ @PreAuthorize("hasAuthority('APP')")
+ @interface MyAnnotation {
+
+ }
+
+ @MyAnnotation
+ public interface ClassAnnotationControllerInterface {
+
+ String methodOne(String paramOne);
+
+ }
+
+ public static class InterfacedAnnotationClassController implements ClassAnnotationControllerInterface {
+
+ public String methodOne(String paramOne) {
+ return "value";
+ }
+
+ }
+
+ public interface MethodAnnotationControllerInterface {
+
+ @MyAnnotation
+ String methodOne(String paramOne);
+
+ }
+
+ public static class InterfacedAnnotationMethodController implements MethodAnnotationControllerInterface {
+
+ public String methodOne(String paramOne) {
+ return "value";
+ }
+
+ }
+
+}
diff --git a/core/src/test/java/org/springframework/security/authorization/method/Jsr250AuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/method/Jsr250AuthorizationManagerTests.java
new file mode 100644
index 00000000000..778738bb4e2
--- /dev/null
+++ b/core/src/test/java/org/springframework/security/authorization/method/Jsr250AuthorizationManagerTests.java
@@ -0,0 +1,278 @@
+/*
+ * Copyright 2002-2021 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.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.function.Supplier;
+
+import javax.annotation.security.DenyAll;
+import javax.annotation.security.PermitAll;
+import javax.annotation.security.RolesAllowed;
+
+import org.junit.Test;
+
+import org.springframework.core.annotation.AnnotationConfigurationException;
+import org.springframework.security.access.intercept.method.MockMethodInvocation;
+import org.springframework.security.authentication.TestAuthentication;
+import org.springframework.security.authentication.TestingAuthenticationToken;
+import org.springframework.security.authorization.AuthorizationDecision;
+import org.springframework.security.core.Authentication;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+
+/**
+ * Tests for {@link Jsr250AuthorizationManager}.
+ *
+ * @author Evgeniy Cheban
+ */
+public class Jsr250AuthorizationManagerTests {
+
+ @Test
+ public void rolePrefixWhenNotSetThenDefaultsToRole() {
+ Jsr250AuthorizationManager manager = new Jsr250AuthorizationManager();
+ assertThat(manager).extracting("rolePrefix").isEqualTo("ROLE_");
+ }
+
+ @Test
+ public void setRolePrefixWhenNullThenException() {
+ Jsr250AuthorizationManager manager = new Jsr250AuthorizationManager();
+ assertThatIllegalArgumentException().isThrownBy(() -> manager.setRolePrefix(null))
+ .withMessage("rolePrefix cannot be null");
+ }
+
+ @Test
+ public void setRolePrefixWhenNotNullThenSets() {
+ Jsr250AuthorizationManager manager = new Jsr250AuthorizationManager();
+ manager.setRolePrefix("CUSTOM_");
+ assertThat(manager).extracting("rolePrefix").isEqualTo("CUSTOM_");
+ }
+
+ @Test
+ public void checkDoSomethingWhenNoJsr250AnnotationsThenNullDecision() throws Exception {
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+ "doSomething");
+ Jsr250AuthorizationManager manager = new Jsr250AuthorizationManager();
+ AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, methodInvocation);
+ assertThat(decision).isNull();
+ }
+
+ @Test
+ public void checkPermitAllWhenRoleUserThenGrantedDecision() throws Exception {
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, "permitAll");
+ Jsr250AuthorizationManager manager = new Jsr250AuthorizationManager();
+ AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, methodInvocation);
+ assertThat(decision).isNotNull();
+ assertThat(decision.isGranted()).isTrue();
+ }
+
+ @Test
+ public void checkDenyAllWhenRoleAdminThenDeniedDecision() throws Exception {
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, "denyAll");
+ Jsr250AuthorizationManager manager = new Jsr250AuthorizationManager();
+ AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedAdmin, methodInvocation);
+ assertThat(decision).isNotNull();
+ assertThat(decision.isGranted()).isFalse();
+ }
+
+ @Test
+ public void checkRolesAllowedUserOrAdminWhenRoleUserThenGrantedDecision() throws Exception {
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+ "rolesAllowedUserOrAdmin");
+ Jsr250AuthorizationManager manager = new Jsr250AuthorizationManager();
+ AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, methodInvocation);
+ assertThat(decision).isNotNull();
+ assertThat(decision.isGranted()).isTrue();
+ }
+
+ @Test
+ public void checkRolesAllowedUserOrAdminWhenRoleAdminThenGrantedDecision() throws Exception {
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+ "rolesAllowedUserOrAdmin");
+ Jsr250AuthorizationManager manager = new Jsr250AuthorizationManager();
+ AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedAdmin, methodInvocation);
+ assertThat(decision).isNotNull();
+ assertThat(decision.isGranted()).isTrue();
+ }
+
+ @Test
+ public void checkRolesAllowedUserOrAdminWhenRoleAnonymousThenDeniedDecision() throws Exception {
+ Supplier authentication = () -> new TestingAuthenticationToken("user", "password",
+ "ROLE_ANONYMOUS");
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+ "rolesAllowedUserOrAdmin");
+ Jsr250AuthorizationManager manager = new Jsr250AuthorizationManager();
+ AuthorizationDecision decision = manager.check(authentication, methodInvocation);
+ assertThat(decision).isNotNull();
+ assertThat(decision.isGranted()).isFalse();
+ }
+
+ @Test
+ public void checkMultipleAnnotationsWhenInvokedThenAnnotationConfigurationException() throws Exception {
+ Supplier authentication = () -> new TestingAuthenticationToken("user", "password",
+ "ROLE_ANONYMOUS");
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+ "multipleAnnotations");
+ Jsr250AuthorizationManager manager = new Jsr250AuthorizationManager();
+ assertThatExceptionOfType(AnnotationConfigurationException.class)
+ .isThrownBy(() -> manager.check(authentication, methodInvocation));
+ }
+
+ @Test
+ public void checkRequiresAdminWhenClassAnnotationsThenMethodAnnotationsTakePrecedence() throws Exception {
+ Supplier authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_USER");
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new ClassLevelAnnotations(),
+ ClassLevelAnnotations.class, "rolesAllowedAdmin");
+ Jsr250AuthorizationManager manager = new Jsr250AuthorizationManager();
+ AuthorizationDecision decision = manager.check(authentication, methodInvocation);
+ assertThat(decision.isGranted()).isFalse();
+ authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_ADMIN");
+ decision = manager.check(authentication, methodInvocation);
+ assertThat(decision.isGranted()).isTrue();
+ }
+
+ @Test
+ public void checkDeniedWhenClassAnnotationsThenMethodAnnotationsTakePrecedence() throws Exception {
+ Supplier authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_USER");
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new ClassLevelAnnotations(),
+ ClassLevelAnnotations.class, "denyAll");
+ Jsr250AuthorizationManager manager = new Jsr250AuthorizationManager();
+ AuthorizationDecision decision = manager.check(authentication, methodInvocation);
+ assertThat(decision.isGranted()).isFalse();
+ }
+
+ @Test
+ public void checkRequiresUserWhenClassAnnotationsThenApplies() throws Exception {
+ Supplier authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_USER");
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new ClassLevelAnnotations(),
+ ClassLevelAnnotations.class, "rolesAllowedUser");
+ Jsr250AuthorizationManager manager = new Jsr250AuthorizationManager();
+ AuthorizationDecision decision = manager.check(authentication, methodInvocation);
+ assertThat(decision.isGranted()).isTrue();
+ authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_ADMIN");
+ decision = manager.check(authentication, methodInvocation);
+ assertThat(decision.isGranted()).isFalse();
+ }
+
+ @Test
+ public void checkInheritedAnnotationsWhenDuplicatedThenAnnotationConfigurationException() throws Exception {
+ Supplier authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_USER");
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+ "inheritedAnnotations");
+ Jsr250AuthorizationManager manager = new Jsr250AuthorizationManager();
+ assertThatExceptionOfType(AnnotationConfigurationException.class)
+ .isThrownBy(() -> manager.check(authentication, methodInvocation));
+ }
+
+ @Test
+ public void checkInheritedAnnotationsWhenConflictingThenAnnotationConfigurationException() throws Exception {
+ Supplier authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_USER");
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new ClassLevelAnnotations(),
+ ClassLevelAnnotations.class, "inheritedAnnotations");
+ Jsr250AuthorizationManager manager = new Jsr250AuthorizationManager();
+ assertThatExceptionOfType(AnnotationConfigurationException.class)
+ .isThrownBy(() -> manager.check(authentication, methodInvocation));
+ }
+
+ public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo {
+
+ public void doSomething() {
+
+ }
+
+ @DenyAll
+ public void denyAll() {
+
+ }
+
+ @PermitAll
+ public void permitAll() {
+
+ }
+
+ @RolesAllowed({ "USER", "ADMIN" })
+ public void rolesAllowedUserOrAdmin() {
+
+ }
+
+ @RolesAllowed("USER")
+ @DenyAll
+ public void multipleAnnotations() {
+
+ }
+
+ public void inheritedAnnotations() {
+
+ }
+
+ }
+
+ @RolesAllowed("USER")
+ public static class ClassLevelAnnotations implements InterfaceAnnotationsThree {
+
+ @RolesAllowed("ADMIN")
+ public void rolesAllowedAdmin() {
+
+ }
+
+ @DenyAll
+ public void denyAll() {
+
+ }
+
+ public void rolesAllowedUser() {
+
+ }
+
+ @Override
+ @PermitAll
+ public void inheritedAnnotations() {
+
+ }
+
+ }
+
+ public interface InterfaceAnnotationsOne {
+
+ @RolesAllowed("ADMIN")
+ void inheritedAnnotations();
+
+ }
+
+ public interface InterfaceAnnotationsTwo {
+
+ @MyRolesAllowed
+ void inheritedAnnotations();
+
+ }
+
+ public interface InterfaceAnnotationsThree {
+
+ @DenyAll
+ void inheritedAnnotations();
+
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @RolesAllowed("USER")
+ public @interface MyRolesAllowed {
+
+ }
+
+}
diff --git a/core/src/test/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManagerTests.java
new file mode 100644
index 00000000000..e6a068a4cf1
--- /dev/null
+++ b/core/src/test/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManagerTests.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright 2002-2021 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.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Supplier;
+
+import org.junit.Test;
+
+import org.springframework.core.annotation.AnnotationConfigurationException;
+import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
+import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
+import org.springframework.security.access.intercept.method.MockMethodInvocation;
+import org.springframework.security.access.prepost.PostAuthorize;
+import org.springframework.security.authentication.TestAuthentication;
+import org.springframework.security.authentication.TestingAuthenticationToken;
+import org.springframework.security.authorization.AuthorizationDecision;
+import org.springframework.security.core.Authentication;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+
+/**
+ * Tests for {@link PostAuthorizeAuthorizationManager}.
+ *
+ * @author Evgeniy Cheban
+ */
+public class PostAuthorizeAuthorizationManagerTests {
+
+ @Test
+ public void setExpressionHandlerWhenNotNullThenSetsExpressionHandler() {
+ MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
+ PostAuthorizeAuthorizationManager manager = new PostAuthorizeAuthorizationManager();
+ manager.setExpressionHandler(expressionHandler);
+ assertThat(manager).extracting("expressionHandler").isEqualTo(expressionHandler);
+ }
+
+ @Test
+ public void setExpressionHandlerWhenNullThenException() {
+ PostAuthorizeAuthorizationManager manager = new PostAuthorizeAuthorizationManager();
+ assertThatIllegalArgumentException().isThrownBy(() -> manager.setExpressionHandler(null))
+ .withMessage("expressionHandler cannot be null");
+ }
+
+ @Test
+ public void checkDoSomethingWhenNoPostAuthorizeAnnotationThenNullDecision() throws Exception {
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+ "doSomething", new Class[] {}, new Object[] {});
+ PostAuthorizeAuthorizationManager manager = new PostAuthorizeAuthorizationManager();
+ MethodInvocationResult result = new MethodInvocationResult(methodInvocation, null);
+ AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, result);
+ assertThat(decision).isNull();
+ }
+
+ @Test
+ public void checkDoSomethingStringWhenArgIsGrantThenGrantedDecision() throws Exception {
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+ "doSomethingString", new Class[] { String.class }, new Object[] { "grant" });
+ PostAuthorizeAuthorizationManager manager = new PostAuthorizeAuthorizationManager();
+ MethodInvocationResult result = new MethodInvocationResult(methodInvocation, null);
+ AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, result);
+ assertThat(decision).isNotNull();
+ assertThat(decision.isGranted()).isTrue();
+ }
+
+ @Test
+ public void checkDoSomethingStringWhenArgIsNotGrantThenDeniedDecision() throws Exception {
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+ "doSomethingString", new Class[] { String.class }, new Object[] { "deny" });
+ MethodInvocationResult result = new MethodInvocationResult(methodInvocation, null);
+ PostAuthorizeAuthorizationManager manager = new PostAuthorizeAuthorizationManager();
+ AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, result);
+ assertThat(decision).isNotNull();
+ assertThat(decision.isGranted()).isFalse();
+ }
+
+ @Test
+ public void checkDoSomethingListWhenReturnObjectContainsGrantThenGrantedDecision() throws Exception {
+ List list = Arrays.asList("grant", "deny");
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+ "doSomethingList", new Class[] { List.class }, new Object[] { list });
+ MethodInvocationResult result = new MethodInvocationResult(methodInvocation, list);
+ PostAuthorizeAuthorizationManager manager = new PostAuthorizeAuthorizationManager();
+ AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, result);
+ assertThat(decision).isNotNull();
+ assertThat(decision.isGranted()).isTrue();
+ }
+
+ @Test
+ public void checkDoSomethingListWhenReturnObjectNotContainsGrantThenDeniedDecision() throws Exception {
+ List list = Collections.singletonList("deny");
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+ "doSomethingList", new Class[] { List.class }, new Object[] { list });
+ MethodInvocationResult result = new MethodInvocationResult(methodInvocation, list);
+ PostAuthorizeAuthorizationManager manager = new PostAuthorizeAuthorizationManager();
+ AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, result);
+ assertThat(decision).isNotNull();
+ assertThat(decision.isGranted()).isFalse();
+ }
+
+ @Test
+ public void checkRequiresAdminWhenClassAnnotationsThenMethodAnnotationsTakePrecedence() throws Exception {
+ Supplier authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_USER");
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new ClassLevelAnnotations(),
+ ClassLevelAnnotations.class, "securedAdmin");
+ MethodInvocationResult result = new MethodInvocationResult(methodInvocation, null);
+ PostAuthorizeAuthorizationManager manager = new PostAuthorizeAuthorizationManager();
+ AuthorizationDecision decision = manager.check(authentication, result);
+ assertThat(decision.isGranted()).isFalse();
+ authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_ADMIN");
+ decision = manager.check(authentication, result);
+ assertThat(decision.isGranted()).isTrue();
+ }
+
+ @Test
+ public void checkRequiresUserWhenClassAnnotationsThenApplies() throws Exception {
+ Supplier authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_USER");
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new ClassLevelAnnotations(),
+ ClassLevelAnnotations.class, "securedUser");
+ MethodInvocationResult result = new MethodInvocationResult(methodInvocation, null);
+ PostAuthorizeAuthorizationManager manager = new PostAuthorizeAuthorizationManager();
+ AuthorizationDecision decision = manager.check(authentication, result);
+ assertThat(decision.isGranted()).isTrue();
+ authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_ADMIN");
+ decision = manager.check(authentication, result);
+ assertThat(decision.isGranted()).isFalse();
+ }
+
+ @Test
+ public void checkInheritedAnnotationsWhenDuplicatedThenAnnotationConfigurationException() throws Exception {
+ Supplier authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_USER");
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+ "inheritedAnnotations");
+ MethodInvocationResult result = new MethodInvocationResult(methodInvocation, null);
+ PostAuthorizeAuthorizationManager manager = new PostAuthorizeAuthorizationManager();
+ assertThatExceptionOfType(AnnotationConfigurationException.class)
+ .isThrownBy(() -> manager.check(authentication, result));
+ }
+
+ @Test
+ public void checkInheritedAnnotationsWhenConflictingThenAnnotationConfigurationException() throws Exception {
+ Supplier authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_USER");
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new ClassLevelAnnotations(),
+ ClassLevelAnnotations.class, "inheritedAnnotations");
+ MethodInvocationResult result = new MethodInvocationResult(methodInvocation, null);
+ PostAuthorizeAuthorizationManager manager = new PostAuthorizeAuthorizationManager();
+ assertThatExceptionOfType(AnnotationConfigurationException.class)
+ .isThrownBy(() -> manager.check(authentication, result));
+ }
+
+ public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo {
+
+ public void doSomething() {
+
+ }
+
+ @PostAuthorize("#s == 'grant'")
+ public String doSomethingString(String s) {
+ return s;
+ }
+
+ @PostAuthorize("returnObject.contains('grant')")
+ public List doSomethingList(List list) {
+ return list;
+ }
+
+ @Override
+ public void inheritedAnnotations() {
+
+ }
+
+ }
+
+ @PostAuthorize("hasRole('USER')")
+ public static class ClassLevelAnnotations implements InterfaceAnnotationsThree {
+
+ @PostAuthorize("hasRole('ADMIN')")
+ public void securedAdmin() {
+
+ }
+
+ public void securedUser() {
+
+ }
+
+ @Override
+ @PostAuthorize("hasRole('ADMIN')")
+ public void inheritedAnnotations() {
+
+ }
+
+ }
+
+ public interface InterfaceAnnotationsOne {
+
+ @PostAuthorize("hasRole('ADMIN')")
+ void inheritedAnnotations();
+
+ }
+
+ public interface InterfaceAnnotationsTwo {
+
+ @PostAuthorize("hasRole('USER')")
+ void inheritedAnnotations();
+
+ }
+
+ public interface InterfaceAnnotationsThree {
+
+ @MyPostAuthorize
+ void inheritedAnnotations();
+
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @PostAuthorize("hasRole('USER')")
+ public @interface MyPostAuthorize {
+
+ }
+
+}
diff --git a/core/src/test/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptorTests.java b/core/src/test/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptorTests.java
new file mode 100644
index 00000000000..8a5c9aa5838
--- /dev/null
+++ b/core/src/test/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptorTests.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright 2002-2021 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.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import org.assertj.core.api.InstanceOfAssertFactories;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import org.springframework.aop.MethodMatcher;
+import org.springframework.core.annotation.AnnotationConfigurationException;
+import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
+import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
+import org.springframework.security.access.intercept.method.MockMethodInvocation;
+import org.springframework.security.access.prepost.PostFilter;
+import org.springframework.security.authentication.TestAuthentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+
+/**
+ * Tests for {@link PostFilterAuthorizationMethodInterceptor}.
+ *
+ * @author Evgeniy Cheban
+ */
+public class PostFilterAuthorizationMethodInterceptorTests {
+
+ @Before
+ public void setUp() {
+ SecurityContextHolder.getContext().setAuthentication(TestAuthentication.authenticatedUser());
+ }
+
+ @After
+ public void tearDown() {
+ SecurityContextHolder.clearContext();
+ }
+
+ @Test
+ public void setExpressionHandlerWhenNotNullThenSetsExpressionHandler() {
+ MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
+ PostFilterAuthorizationMethodInterceptor advice = new PostFilterAuthorizationMethodInterceptor();
+ advice.setExpressionHandler(expressionHandler);
+ assertThat(advice).extracting("expressionHandler").isEqualTo(expressionHandler);
+ }
+
+ @Test
+ public void setExpressionHandlerWhenNullThenException() {
+ PostFilterAuthorizationMethodInterceptor advice = new PostFilterAuthorizationMethodInterceptor();
+ assertThatIllegalArgumentException().isThrownBy(() -> advice.setExpressionHandler(null))
+ .withMessage("expressionHandler cannot be null");
+ }
+
+ @Test
+ public void methodMatcherWhenMethodHasNotPostFilterAnnotationThenNotMatches() throws Exception {
+ PostFilterAuthorizationMethodInterceptor advice = new PostFilterAuthorizationMethodInterceptor();
+ MethodMatcher methodMatcher = advice.getPointcut().getMethodMatcher();
+ assertThat(methodMatcher.matches(NoPostFilterClass.class.getMethod("doSomething"), NoPostFilterClass.class))
+ .isFalse();
+ }
+
+ @Test
+ public void methodMatcherWhenMethodHasPostFilterAnnotationThenMatches() throws Exception {
+ PostFilterAuthorizationMethodInterceptor advice = new PostFilterAuthorizationMethodInterceptor();
+ MethodMatcher methodMatcher = advice.getPointcut().getMethodMatcher();
+ assertThat(
+ methodMatcher.matches(TestClass.class.getMethod("doSomethingArray", String[].class), TestClass.class))
+ .isTrue();
+ }
+
+ @Test
+ public void afterWhenArrayNotNullThenFilteredArray() throws Throwable {
+ String[] array = { "john", "bob" };
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+ "doSomethingArrayClassLevel", new Class[] { String[].class }, new Object[] { array }) {
+ @Override
+ public Object proceed() {
+ return array;
+ }
+ };
+ PostFilterAuthorizationMethodInterceptor advice = new PostFilterAuthorizationMethodInterceptor();
+ Object result = advice.invoke(methodInvocation);
+ assertThat(result).asInstanceOf(InstanceOfAssertFactories.array(String[].class)).containsOnly("john");
+ }
+
+ @Test
+ public void checkInheritedAnnotationsWhenDuplicatedThenAnnotationConfigurationException() throws Exception {
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+ "inheritedAnnotations");
+ PostFilterAuthorizationMethodInterceptor advice = new PostFilterAuthorizationMethodInterceptor();
+ assertThatExceptionOfType(AnnotationConfigurationException.class)
+ .isThrownBy(() -> advice.invoke(methodInvocation));
+ }
+
+ @Test
+ public void checkInheritedAnnotationsWhenConflictingThenAnnotationConfigurationException() throws Exception {
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new ConflictingAnnotations(),
+ ConflictingAnnotations.class, "inheritedAnnotations");
+ PostFilterAuthorizationMethodInterceptor advice = new PostFilterAuthorizationMethodInterceptor();
+ assertThatExceptionOfType(AnnotationConfigurationException.class)
+ .isThrownBy(() -> advice.invoke(methodInvocation));
+ }
+
+ @PostFilter("filterObject == 'john'")
+ public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo {
+
+ @PostFilter("filterObject == 'john'")
+ public String[] doSomethingArray(String[] array) {
+ return array;
+ }
+
+ public String[] doSomethingArrayClassLevel(String[] array) {
+ return array;
+ }
+
+ @Override
+ public void inheritedAnnotations() {
+
+ }
+
+ }
+
+ public static class NoPostFilterClass {
+
+ public void doSomething() {
+
+ }
+
+ }
+
+ public static class ConflictingAnnotations implements InterfaceAnnotationsThree {
+
+ @Override
+ @PostFilter("filterObject == 'jack'")
+ public void inheritedAnnotations() {
+
+ }
+
+ }
+
+ public interface InterfaceAnnotationsOne {
+
+ @PostFilter("filterObject == 'jim'")
+ void inheritedAnnotations();
+
+ }
+
+ public interface InterfaceAnnotationsTwo {
+
+ @PostFilter("filterObject == 'jane'")
+ void inheritedAnnotations();
+
+ }
+
+ public interface InterfaceAnnotationsThree {
+
+ @MyPostFilter
+ void inheritedAnnotations();
+
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @PostFilter("filterObject == 'john'")
+ public @interface MyPostFilter {
+
+ }
+
+}
diff --git a/core/src/test/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManagerTests.java
new file mode 100644
index 00000000000..0247617cfb7
--- /dev/null
+++ b/core/src/test/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManagerTests.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright 2002-2021 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.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.function.Supplier;
+
+import org.junit.Test;
+
+import org.springframework.core.annotation.AnnotationConfigurationException;
+import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
+import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
+import org.springframework.security.access.intercept.method.MockMethodInvocation;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.authentication.TestAuthentication;
+import org.springframework.security.authentication.TestingAuthenticationToken;
+import org.springframework.security.authorization.AuthorizationDecision;
+import org.springframework.security.core.Authentication;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+
+/**
+ * Tests for {@link PreAuthorizeAuthorizationManager}.
+ *
+ * @author Evgeniy Cheban
+ */
+public class PreAuthorizeAuthorizationManagerTests {
+
+ @Test
+ public void setExpressionHandlerWhenNotNullThenSetsExpressionHandler() {
+ MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
+ PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager();
+ manager.setExpressionHandler(expressionHandler);
+ assertThat(manager).extracting("expressionHandler").isEqualTo(expressionHandler);
+ }
+
+ @Test
+ public void setExpressionHandlerWhenNullThenException() {
+ PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager();
+ assertThatIllegalArgumentException().isThrownBy(() -> manager.setExpressionHandler(null))
+ .withMessage("expressionHandler cannot be null");
+ }
+
+ @Test
+ public void checkDoSomethingWhenNoPostAuthorizeAnnotationThenNullDecision() throws Exception {
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+ "doSomething", new Class[] {}, new Object[] {});
+ PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager();
+ AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, methodInvocation);
+ assertThat(decision).isNull();
+ }
+
+ @Test
+ public void checkDoSomethingStringWhenArgIsGrantThenGrantedDecision() throws Exception {
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+ "doSomethingString", new Class[] { String.class }, new Object[] { "grant" });
+ PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager();
+ AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, methodInvocation);
+ assertThat(decision).isNotNull();
+ assertThat(decision.isGranted()).isTrue();
+ }
+
+ @Test
+ public void checkDoSomethingStringWhenArgIsNotGrantThenDeniedDecision() throws Exception {
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+ "doSomethingString", new Class[] { String.class }, new Object[] { "deny" });
+ PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager();
+ AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, methodInvocation);
+ assertThat(decision).isNotNull();
+ assertThat(decision.isGranted()).isFalse();
+ }
+
+ @Test
+ public void checkRequiresAdminWhenClassAnnotationsThenMethodAnnotationsTakePrecedence() throws Exception {
+ Supplier authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_USER");
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new ClassLevelAnnotations(),
+ ClassLevelAnnotations.class, "securedAdmin");
+ PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager();
+ AuthorizationDecision decision = manager.check(authentication, methodInvocation);
+ assertThat(decision.isGranted()).isFalse();
+ authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_ADMIN");
+ decision = manager.check(authentication, methodInvocation);
+ assertThat(decision.isGranted()).isTrue();
+ }
+
+ @Test
+ public void checkRequiresUserWhenClassAnnotationsThenApplies() throws Exception {
+ Supplier authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_USER");
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new ClassLevelAnnotations(),
+ ClassLevelAnnotations.class, "securedUser");
+ PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager();
+ AuthorizationDecision decision = manager.check(authentication, methodInvocation);
+ assertThat(decision.isGranted()).isTrue();
+ authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_ADMIN");
+ decision = manager.check(authentication, methodInvocation);
+ assertThat(decision.isGranted()).isFalse();
+ }
+
+ @Test
+ public void checkInheritedAnnotationsWhenDuplicatedThenAnnotationConfigurationException() throws Exception {
+ Supplier authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_USER");
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+ "inheritedAnnotations");
+ PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager();
+ assertThatExceptionOfType(AnnotationConfigurationException.class)
+ .isThrownBy(() -> manager.check(authentication, methodInvocation));
+ }
+
+ @Test
+ public void checkInheritedAnnotationsWhenConflictingThenAnnotationConfigurationException() throws Exception {
+ Supplier authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_USER");
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new ClassLevelAnnotations(),
+ ClassLevelAnnotations.class, "inheritedAnnotations");
+ PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager();
+ assertThatExceptionOfType(AnnotationConfigurationException.class)
+ .isThrownBy(() -> manager.check(authentication, methodInvocation));
+ }
+
+ public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo {
+
+ public void doSomething() {
+
+ }
+
+ @PreAuthorize("#s == 'grant'")
+ public String doSomethingString(String s) {
+ return s;
+ }
+
+ @Override
+ public void inheritedAnnotations() {
+
+ }
+
+ }
+
+ @PreAuthorize("hasRole('USER')")
+ public static class ClassLevelAnnotations implements InterfaceAnnotationsThree {
+
+ @PreAuthorize("hasRole('ADMIN')")
+ public void securedAdmin() {
+
+ }
+
+ public void securedUser() {
+
+ }
+
+ @Override
+ @PreAuthorize("hasRole('ADMIN')")
+ public void inheritedAnnotations() {
+
+ }
+
+ }
+
+ public interface InterfaceAnnotationsOne {
+
+ @PreAuthorize("hasRole('ADMIN')")
+ void inheritedAnnotations();
+
+ }
+
+ public interface InterfaceAnnotationsTwo {
+
+ @PreAuthorize("hasRole('USER')")
+ void inheritedAnnotations();
+
+ }
+
+ public interface InterfaceAnnotationsThree {
+
+ @MyPreAuthorize
+ void inheritedAnnotations();
+
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @PreAuthorize("hasRole('USER')")
+ public @interface MyPreAuthorize {
+
+ }
+
+}
diff --git a/core/src/test/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptorTests.java b/core/src/test/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptorTests.java
new file mode 100644
index 00000000000..78432a5e5db
--- /dev/null
+++ b/core/src/test/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptorTests.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright 2002-2021 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.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import org.springframework.aop.MethodMatcher;
+import org.springframework.core.annotation.AnnotationConfigurationException;
+import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
+import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
+import org.springframework.security.access.intercept.method.MockMethodInvocation;
+import org.springframework.security.access.prepost.PreFilter;
+import org.springframework.security.authentication.TestAuthentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
+
+/**
+ * Tests for {@link PreFilterAuthorizationMethodInterceptor}.
+ *
+ * @author Evgeniy Cheban
+ */
+public class PreFilterAuthorizationMethodInterceptorTests {
+
+ @Before
+ public void setUp() {
+ SecurityContextHolder.getContext().setAuthentication(TestAuthentication.authenticatedUser());
+ }
+
+ @After
+ public void tearDown() {
+ SecurityContextHolder.clearContext();
+ }
+
+ @Test
+ public void setExpressionHandlerWhenNotNullThenSetsExpressionHandler() {
+ MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
+ PreFilterAuthorizationMethodInterceptor advice = new PreFilterAuthorizationMethodInterceptor();
+ advice.setExpressionHandler(expressionHandler);
+ assertThat(advice).extracting("expressionHandler").isEqualTo(expressionHandler);
+ }
+
+ @Test
+ public void setExpressionHandlerWhenNullThenException() {
+ PreFilterAuthorizationMethodInterceptor advice = new PreFilterAuthorizationMethodInterceptor();
+ assertThatIllegalArgumentException().isThrownBy(() -> advice.setExpressionHandler(null))
+ .withMessage("expressionHandler cannot be null");
+ }
+
+ @Test
+ public void methodMatcherWhenMethodHasNotPreFilterAnnotationThenNotMatches() throws Exception {
+ PreFilterAuthorizationMethodInterceptor advice = new PreFilterAuthorizationMethodInterceptor();
+ MethodMatcher methodMatcher = advice.getPointcut().getMethodMatcher();
+ assertThat(methodMatcher.matches(NoPreFilterClass.class.getMethod("doSomething"), NoPreFilterClass.class))
+ .isFalse();
+ }
+
+ @Test
+ public void methodMatcherWhenMethodHasPreFilterAnnotationThenMatches() throws Exception {
+ PreFilterAuthorizationMethodInterceptor advice = new PreFilterAuthorizationMethodInterceptor();
+ MethodMatcher methodMatcher = advice.getPointcut().getMethodMatcher();
+ assertThat(methodMatcher.matches(TestClass.class.getMethod("doSomethingListFilterTargetMatch", List.class),
+ TestClass.class)).isTrue();
+ }
+
+ @Test
+ public void findFilterTargetWhenNameProvidedAndNotMatchThenException() throws Exception {
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+ "doSomethingListFilterTargetNotMatch", new Class[] { List.class }, new Object[] { new ArrayList<>() });
+ PreFilterAuthorizationMethodInterceptor advice = new PreFilterAuthorizationMethodInterceptor();
+ assertThatIllegalArgumentException().isThrownBy(() -> advice.invoke(methodInvocation)).withMessage(
+ "Filter target was null, or no argument with name 'filterTargetNotMatch' found in method.");
+ }
+
+ @Test
+ public void findFilterTargetWhenNameProvidedAndMatchAndNullThenException() throws Exception {
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+ "doSomethingListFilterTargetMatch", new Class[] { List.class }, new Object[] { null });
+ PreFilterAuthorizationMethodInterceptor advice = new PreFilterAuthorizationMethodInterceptor();
+ assertThatIllegalArgumentException().isThrownBy(() -> advice.invoke(methodInvocation))
+ .withMessage("Filter target was null, or no argument with name 'list' found in method.");
+ }
+
+ @Test
+ public void findFilterTargetWhenNameProvidedAndMatchAndNotNullThenFiltersList() throws Throwable {
+ List list = new ArrayList<>();
+ list.add("john");
+ list.add("bob");
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+ "doSomethingListFilterTargetMatch", new Class[] { List.class }, new Object[] { list });
+ PreFilterAuthorizationMethodInterceptor advice = new PreFilterAuthorizationMethodInterceptor();
+ advice.invoke(methodInvocation);
+ assertThat(list).hasSize(1);
+ assertThat(list.get(0)).isEqualTo("john");
+ }
+
+ @Test
+ public void findFilterTargetWhenNameNotProvidedAndSingleArgListNullThenException() throws Exception {
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+ "doSomethingListFilterTargetNotProvided", new Class[] { List.class }, new Object[] { null });
+ PreFilterAuthorizationMethodInterceptor advice = new PreFilterAuthorizationMethodInterceptor();
+ assertThatIllegalArgumentException().isThrownBy(() -> advice.invoke(methodInvocation))
+ .withMessage("Filter target was null. Make sure you passing the correct value in the method argument.");
+ }
+
+ @Test
+ public void findFilterTargetWhenNameNotProvidedAndSingleArgListThenFiltersList() throws Throwable {
+ List list = new ArrayList<>();
+ list.add("john");
+ list.add("bob");
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+ "doSomethingListFilterTargetNotProvided", new Class[] { List.class }, new Object[] { list });
+ PreFilterAuthorizationMethodInterceptor advice = new PreFilterAuthorizationMethodInterceptor();
+ advice.invoke(methodInvocation);
+ assertThat(list).hasSize(1);
+ assertThat(list.get(0)).isEqualTo("john");
+ }
+
+ @Test
+ public void findFilterTargetWhenNameNotProvidedAndSingleArgArrayThenException() throws Exception {
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+ "doSomethingArrayFilterTargetNotProvided", new Class[] { String[].class },
+ new Object[] { new String[] {} });
+ PreFilterAuthorizationMethodInterceptor advice = new PreFilterAuthorizationMethodInterceptor();
+ assertThatIllegalStateException().isThrownBy(() -> advice.invoke(methodInvocation)).withMessage(
+ "Pre-filtering on array types is not supported. Using a Collection will solve this problem.");
+ }
+
+ @Test
+ public void findFilterTargetWhenNameNotProvidedAndNotSingleArgThenException() throws Exception {
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+ "doSomethingTwoArgsFilterTargetNotProvided", new Class[] { String.class, List.class },
+ new Object[] { "", new ArrayList<>() });
+ PreFilterAuthorizationMethodInterceptor advice = new PreFilterAuthorizationMethodInterceptor();
+ assertThatIllegalStateException().isThrownBy(() -> advice.invoke(methodInvocation))
+ .withMessage("Unable to determine the method argument for filtering. Specify the filter target.");
+ }
+
+ @Test
+ public void checkInheritedAnnotationsWhenDuplicatedThenAnnotationConfigurationException() throws Exception {
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+ "inheritedAnnotations");
+ PreFilterAuthorizationMethodInterceptor advice = new PreFilterAuthorizationMethodInterceptor();
+ assertThatExceptionOfType(AnnotationConfigurationException.class)
+ .isThrownBy(() -> advice.invoke(methodInvocation));
+ }
+
+ @Test
+ public void checkInheritedAnnotationsWhenConflictingThenAnnotationConfigurationException() throws Exception {
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new ConflictingAnnotations(),
+ ConflictingAnnotations.class, "inheritedAnnotations");
+ PreFilterAuthorizationMethodInterceptor advice = new PreFilterAuthorizationMethodInterceptor();
+ assertThatExceptionOfType(AnnotationConfigurationException.class)
+ .isThrownBy(() -> advice.invoke(methodInvocation));
+ }
+
+ @PreFilter("filterObject == 'john'")
+ public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo {
+
+ @PreFilter(value = "filterObject == 'john'", filterTarget = "filterTargetNotMatch")
+ public List doSomethingListFilterTargetNotMatch(List list) {
+ return list;
+ }
+
+ @PreFilter(value = "filterObject == 'john'", filterTarget = "list")
+ public List doSomethingListFilterTargetMatch(List list) {
+ return list;
+ }
+
+ @PreFilter("filterObject == 'john'")
+ public List doSomethingListFilterTargetNotProvided(List list) {
+ return list;
+ }
+
+ @PreFilter("filterObject == 'john'")
+ public String[] doSomethingArrayFilterTargetNotProvided(String[] array) {
+ return array;
+ }
+
+ public List doSomethingTwoArgsFilterTargetNotProvided(String s, List list) {
+ return list;
+ }
+
+ @Override
+ public void inheritedAnnotations() {
+
+ }
+
+ }
+
+ public static class NoPreFilterClass {
+
+ public void doSomething() {
+
+ }
+
+ }
+
+ public static class ConflictingAnnotations implements InterfaceAnnotationsThree {
+
+ @Override
+ @PreFilter("filterObject == 'jack'")
+ public void inheritedAnnotations() {
+
+ }
+
+ }
+
+ public interface InterfaceAnnotationsOne {
+
+ @PreFilter("filterObject == 'jim'")
+ void inheritedAnnotations();
+
+ }
+
+ public interface InterfaceAnnotationsTwo {
+
+ @PreFilter("filterObject == 'jane'")
+ void inheritedAnnotations();
+
+ }
+
+ public interface InterfaceAnnotationsThree {
+
+ @MyPreFilter
+ void inheritedAnnotations();
+
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @PreFilter("filterObject == 'john'")
+ public @interface MyPreFilter {
+
+ }
+
+}
diff --git a/core/src/test/java/org/springframework/security/authorization/method/SecuredAuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/method/SecuredAuthorizationManagerTests.java
new file mode 100644
index 00000000000..3f1e5077318
--- /dev/null
+++ b/core/src/test/java/org/springframework/security/authorization/method/SecuredAuthorizationManagerTests.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright 2002-2021 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.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.function.Supplier;
+
+import org.junit.Test;
+
+import org.springframework.core.annotation.AnnotationConfigurationException;
+import org.springframework.security.access.annotation.Secured;
+import org.springframework.security.access.intercept.method.MockMethodInvocation;
+import org.springframework.security.authentication.TestAuthentication;
+import org.springframework.security.authentication.TestingAuthenticationToken;
+import org.springframework.security.authorization.AuthorizationDecision;
+import org.springframework.security.core.Authentication;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+
+/**
+ * Tests for {@link SecuredAuthorizationManager}.
+ *
+ * @author Evgeniy Cheban
+ */
+public class SecuredAuthorizationManagerTests {
+
+ @Test
+ public void checkDoSomethingWhenNoSecuredAnnotationThenNullDecision() throws Exception {
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+ "doSomething");
+ SecuredAuthorizationManager manager = new SecuredAuthorizationManager();
+ AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, methodInvocation);
+ assertThat(decision).isNull();
+ }
+
+ @Test
+ public void checkSecuredUserOrAdminWhenRoleUserThenGrantedDecision() throws Exception {
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+ "securedUserOrAdmin");
+ SecuredAuthorizationManager manager = new SecuredAuthorizationManager();
+ AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, methodInvocation);
+ assertThat(decision).isNotNull();
+ assertThat(decision.isGranted()).isTrue();
+ }
+
+ @Test
+ public void checkSecuredUserOrAdminWhenRoleAdminThenGrantedDecision() throws Exception {
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+ "securedUserOrAdmin");
+ SecuredAuthorizationManager manager = new SecuredAuthorizationManager();
+ AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedAdmin, methodInvocation);
+ assertThat(decision).isNotNull();
+ assertThat(decision.isGranted()).isTrue();
+ }
+
+ @Test
+ public void checkSecuredUserOrAdminWhenRoleAnonymousThenDeniedDecision() throws Exception {
+ Supplier authentication = () -> new TestingAuthenticationToken("user", "password",
+ "ROLE_ANONYMOUS");
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+ "securedUserOrAdmin");
+ SecuredAuthorizationManager manager = new SecuredAuthorizationManager();
+ AuthorizationDecision decision = manager.check(authentication, methodInvocation);
+ assertThat(decision).isNotNull();
+ assertThat(decision.isGranted()).isFalse();
+ }
+
+ @Test
+ public void checkRequiresAdminWhenClassAnnotationsThenMethodAnnotationsTakePrecedence() throws Exception {
+ Supplier authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_USER");
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new ClassLevelAnnotations(),
+ ClassLevelAnnotations.class, "securedAdmin");
+ SecuredAuthorizationManager manager = new SecuredAuthorizationManager();
+ AuthorizationDecision decision = manager.check(authentication, methodInvocation);
+ assertThat(decision.isGranted()).isFalse();
+ authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_ADMIN");
+ decision = manager.check(authentication, methodInvocation);
+ assertThat(decision.isGranted()).isTrue();
+ }
+
+ @Test
+ public void checkRequiresUserWhenClassAnnotationsThenApplies() throws Exception {
+ Supplier authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_USER");
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new ClassLevelAnnotations(),
+ ClassLevelAnnotations.class, "securedUser");
+ SecuredAuthorizationManager manager = new SecuredAuthorizationManager();
+ AuthorizationDecision decision = manager.check(authentication, methodInvocation);
+ assertThat(decision.isGranted()).isTrue();
+ authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_ADMIN");
+ decision = manager.check(authentication, methodInvocation);
+ assertThat(decision.isGranted()).isFalse();
+ }
+
+ @Test
+ public void checkInheritedAnnotationsWhenDuplicatedThenAnnotationConfigurationException() throws Exception {
+ Supplier authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_USER");
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
+ "inheritedAnnotations");
+ SecuredAuthorizationManager manager = new SecuredAuthorizationManager();
+ assertThatExceptionOfType(AnnotationConfigurationException.class)
+ .isThrownBy(() -> manager.check(authentication, methodInvocation));
+ }
+
+ @Test
+ public void checkInheritedAnnotationsWhenConflictingThenAnnotationConfigurationException() throws Exception {
+ Supplier authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_USER");
+ MockMethodInvocation methodInvocation = new MockMethodInvocation(new ClassLevelAnnotations(),
+ ClassLevelAnnotations.class, "inheritedAnnotations");
+ SecuredAuthorizationManager manager = new SecuredAuthorizationManager();
+ assertThatExceptionOfType(AnnotationConfigurationException.class)
+ .isThrownBy(() -> manager.check(authentication, methodInvocation));
+ }
+
+ public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo {
+
+ public void doSomething() {
+
+ }
+
+ @Secured({ "ROLE_USER", "ROLE_ADMIN" })
+ public void securedUserOrAdmin() {
+
+ }
+
+ @Override
+ public void inheritedAnnotations() {
+
+ }
+
+ }
+
+ @Secured("ROLE_USER")
+ public static class ClassLevelAnnotations implements InterfaceAnnotationsThree {
+
+ @Secured("ROLE_ADMIN")
+ public void securedAdmin() {
+
+ }
+
+ public void securedUser() {
+
+ }
+
+ @Override
+ @Secured("ROLE_ADMIN")
+ public void inheritedAnnotations() {
+
+ }
+
+ }
+
+ public interface InterfaceAnnotationsOne {
+
+ @Secured("ROLE_ADMIN")
+ void inheritedAnnotations();
+
+ }
+
+ public interface InterfaceAnnotationsTwo {
+
+ @Secured("ROLE_USER")
+ void inheritedAnnotations();
+
+ }
+
+ public interface InterfaceAnnotationsThree {
+
+ @MySecured
+ void inheritedAnnotations();
+
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Secured("ROLE_USER")
+ public @interface MySecured {
+
+ }
+
+}
diff --git a/docs/manual/src/docs/asciidoc/_includes/about/whats-new.adoc b/docs/manual/src/docs/asciidoc/_includes/about/whats-new.adoc
index 091b99142f9..05d0ba04979 100644
--- a/docs/manual/src/docs/asciidoc/_includes/about/whats-new.adoc
+++ b/docs/manual/src/docs/asciidoc/_includes/about/whats-new.adoc
@@ -1,51 +1,11 @@
[[new]]
-== What's New in Spring Security 5.5
+== What's New in Spring Security 5.6
-Spring Security 5.5 provides a number of new features.
+Spring Security 5.6 provides a number of new features.
Below are the highlights of the release.
[[whats-new-servlet]]
=== Servlet
-* OAuth 2.0 Client
-
-** Added support for https://github.com/spring-projects/spring-security/pull/9520[Jwt Client Authentication] `private_key_jwt` and `client_secret_jwt`
-** Added https://github.com/spring-projects/spring-security/pull/9535[Jwt Bearer Authorization Grant] support
-** Added https://github.com/spring-projects/spring-security/pull/8765[R2DBC implementation] of `ReactiveOAuth2AuthorizedClientService`
-
-* OAuth 2.0 Resource Server
-
-** Enhanced JWT decoders https://github.com/spring-projects/spring-security/issues/7160[derivation of signature algorithms]
-** Improved https://github.com/spring-projects/spring-security/issues/9100[content negotation]
-** Improved https://github.com/spring-projects/spring-security/issues/9186[multi-tenancy support]
-
-* SAML 2.0 Service Provider
-
-** Added https://github.com/spring-projects/spring-security/issues/9095[OpenSAML 4 support]
-** Enhanced SAML 2.0 https://github.com/spring-projects/spring-security/issues/9131[Assertion] https://github.com/spring-projects/spring-security/issues/9044[decryption]
-** Added https://github.com/spring-projects/spring-security/issues/9028[file-based configuration] for asserting party metadata
-** Improved https://github.com/spring-projects/spring-security/issues/9486[`RelyingPartyRegistration` resolution] support
-** Enhanced relying party metadata https://github.com/spring-projects/spring-security/issues/9317[support]
-** Added support for https://github.com/spring-projects/spring-security/issues/9177[AP-specified signing methods]
-
* Configuration
-** Introduced https://github.com/spring-projects/spring-security/issues/9205[DispatcherType request matcher]
-** Introduced https://github.com/spring-projects/spring-security/issues/8900[`AuthorizationManager`] for filter security
-
-* Kotlin DSL
-
-** Added https://github.com/spring-projects/spring-security/issues/9319[rememberMe support]
-
-[[whats-new-webflux]]
-=== WebFlux
-
-** Added https://github.com/spring-projects/spring-security/issues/8143[Kotlin coroutine support] for `EnableReactiveMethodSecurity`
-
-[[whats-new-build]]
-=== Build
-
-The build has been modernized in a number of different ways.
-Here are a couple of highlights:
-
-* All sample applications have been moved to https://github.com/spring-projects/spring-security-samples[a separate project]
-* The full build now https://github.com/spring-projects/spring-security/issues/9419[works with JDK 11]
+** Introduced https://github.com/spring-projects/spring-security/pull/9630[`AuthorizationManager`] for method security
diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/appendix/namespace.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/appendix/namespace.adoc
index f65ec3349de..3d5b0dca6b7 100644
--- a/docs/manual/src/docs/asciidoc/_includes/servlet/appendix/namespace.adoc
+++ b/docs/manual/src/docs/asciidoc/_includes/servlet/appendix/namespace.adoc
@@ -759,6 +759,7 @@ A default implementation (with no ACL support) will be used if not supplied.
* <>
* <>
+* <>
* <>
@@ -2374,9 +2375,41 @@ If omitted, the namespace will generate a random value, preventing its accidenta
Cannot be empty.
-[[nsa-method-security]]
+
=== Method Security
+[[nsa-method-security]]
+====
+This element is the primary means of adding support for securing methods on Spring Security beans.
+Methods can be secured by the use of annotations (defined at the interface or class level) or by defining a set of pointcuts.
+
+[[nsa-method-security-attributes]]
+===== attributes
+
+[[nsa-method-security-pre-post-enabled]]
+* **pre-post-enabled**
+Enables Spring Security's pre and post invocation annotations (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) for this application context.
+Defaults to "true".
+
+[[nsa-method-security-secured-enabled]]
+* **secured-enabled**
+Enables Spring Security's @Secured annotation for this application context.
+Defaults to "false".
+
+[[nsa-method-security-jsr250-enabled]]
+* **jsr250-enabled**
+Enables JSR-250 authorization annotations (@RolesAllowed, @PermitAll, @DenyAll) for this application context.
+Defaults to "false".
+
+[[nsa-method-security-proxy-target-class]]
+* **proxy-target-class**
+If true, class based proxying will be used instead of interface based proxying.
+Defaults to "false".
+
+[[nsa-method-security-children]]
+===== Child Elements of
+
+* <>
[[nsa-global-method-security]]
====
diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/authorization/method-security.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/authorization/method-security.adoc
index 573412f6bf9..a05b62dfc7a 100644
--- a/docs/manual/src/docs/asciidoc/_includes/servlet/authorization/method-security.adoc
+++ b/docs/manual/src/docs/asciidoc/_includes/servlet/authorization/method-security.adoc
@@ -6,6 +6,598 @@ It provides support for JSR-250 annotation security as well as the framework's o
From 3.0 you can also make use of new <>.
You can apply security to a single bean, using the `intercept-methods` element to decorate the bean declaration, or you can secure multiple beans across the entire service layer using the AspectJ style pointcuts.
+=== EnableMethodSecurity
+
+In Spring Security 5.6, we can enable annotation-based security using the `@EnableMethodSecurity` annotation on any `@Configuration` instance.
+
+This improves upon `@EnableGlobalMethodSecurity` in a number of ways. `@EnableMethodSecurity`:
+
+1. Uses the simplified `AuthorizationManager` API instead of metadata sources, config attributes, decision managers, and voters.
+This simplifies reuse and customization.
+2. Favors direct bean-based configuration, instead of requiring extending `GlobalMethodSecurityConfiguration` to customize beans
+3. Is built using native Spring AOP, removing abstractions and allowing you to use Spring AOP building blocks to customize
+4. Checks for conflicting annotations to ensure an unambiguous security configuration
+5. Complies with JSR-250
+6. Enables `@PreAuthorize`, `@PostAuthorize`, `@PreFilter`, and `@PostFilter` by default
+
+[NOTE]
+====
+For earlier versions, please read about similar support with <>.
+====
+
+For example, the following would enable Spring Security's `@PreAuthorize` annotation:
+
+.Method Security Configuration
+====
+.Java
+[source,java,role="primary"]
+----
+@EnableMethodSecurity
+public class MethodSecurityConfig {
+ // ...
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@EnableMethodSecurity
+class MethodSecurityConfig {
+ // ...
+}
+----
+
+.Xml
+[source,xml,role="secondary"]
+----
+
+----
+====
+
+Adding an annotation to a method (on a class or interface) would then limit the access to that method accordingly.
+Spring Security's native annotation support defines a set of attributes for the method.
+These will be passed to the `DefaultAuthorizationMethodInterceptorChain` for it to make the actual decision:
+
+.Method Security Annotation Usage
+====
+.Java
+[source,java,role="primary"]
+----
+public interface BankService {
+ @PreAuthorize("hasRole('USER')")
+ Account readAccount(Long id);
+
+ @PreAuthorize("hasRole('USER')")
+ List findAccounts();
+
+ @PreAuthorize("hasRole('TELLER')")
+ Account post(Account account, Double amount);
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+interface BankService {
+ @PreAuthorize("hasRole('USER')")
+ fun readAccount(id : Long) : Account
+
+ @PreAuthorize("hasRole('USER')")
+ fun findAccounts() : List
+
+ @PreAuthorize("hasRole('TELLER')")
+ fun post(account : Account, amount : Double) : Account
+}
+----
+====
+
+You can enable support for Spring Security's `@Secured` annotation using:
+
+.@Secured Configuration
+====
+.Java
+[source,java,role="primary"]
+----
+@EnableMethodSecurity(securedEnabled = true)
+public class MethodSecurityConfig {
+ // ...
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@EnableMethodSecurity(securedEnabled = true)
+class MethodSecurityConfig {
+ // ...
+}
+----
+
+.Xml
+[source,xml,role="secondary"]
+----
+
+----
+====
+
+or JSR-250 using:
+
+.JSR-250 Configuration
+====
+.Java
+[source,java,role="primary"]
+----
+@EnableMethodSecurity(jsr250Enabled = true)
+public class MethodSecurityConfig {
+ // ...
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@EnableMethodSecurity(jsr250Enabled = true)
+class MethodSecurityConfig {
+ // ...
+}
+----
+
+.Xml
+[source,xml,role="secondary"]
+----
+
+----
+====
+
+==== Customizing Authorization
+
+Spring Security's `@PreAuthorize`, `@PostAuthorize`, `@PreFilter`, and `@PostFilter` ship with rich expression-based support.
+
+[[jc-method-security-custom-expression-handler]]
+If you need to customize the way that expressions are handled, you can expose a custom `MethodSecurityExpressionHandler`, like so:
+
+.Custom MethodSecurityExpressionHandler
+====
+.Java
+[source,java,role="primary"]
+----
+@Bean
+static MethodSecurityExpressionHandler methodSecurityExpressionHandler() {
+ DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
+ handler.setTrustResolver(myCustomTrustResolver);
+ return handler;
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+companion object {
+ @Bean
+ fun methodSecurityExpressionHandler() : MethodSecurityExpressionHandler {
+ val handler = DefaultMethodSecurityExpressionHandler();
+ handler.setTrustResolver(myCustomTrustResolver);
+ return handler;
+ }
+}
+----
+
+.Xml
+[source,xml,role="secondary"]
+----
+
+
+
+
+
+
+
+----
+====
+
+[TIP]
+====
+We expose `MethodSecurityExpressionHandler` using a `static` method to ensure that Spring publishes it before it initializes Spring Security's method security `@Configuration` classes
+====
+
+Also, for role-based authorization, Spring Security adds a default `ROLE_` prefix, which is uses when evaluating expressions like `hasRole`.
+
+[[jc-method-security-custom-granted-authority-defaults]]
+You can configure the authorization rules to use a different prefix by exposing a `GrantedAuthorityDefaults` bean, like so:
+
+.Custom MethodSecurityExpressionHandler
+====
+.Java
+[source,java,role="primary"]
+----
+@Bean
+static GrantedAuthorityDefaults grantedAuthorityDefaults() {
+ return new GrantedAuthorityDefaults("MYPREFIX_");
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+companion object {
+ @Bean
+ fun grantedAuthorityDefaults() : GrantedAuthorityDefaults {
+ return GrantedAuthorityDefaults("MYPREFIX_");
+ }
+}
+----
+
+.Xml
+[source,xml,role="secondary"]
+----
+
+
+
+
+
+----
+====
+
+[TIP]
+====
+We expose `GrantedAuthorityDefaults` using a `static` method to ensure that Spring publishes it before it initializes Spring Security's method security `@Configuration` classes
+====
+
+[[jc-method-security-custom-authorization-manager]]
+==== Custom Authorization Managers
+
+Method authorization is a combination of before- and after-method authorization.
+
+[NOTE]
+====
+Before-method authorization is performed before the method is invoked.
+If that authorization denies access, the method is not invoked, and an `AccessDeniedException` is thrown
+After-method authorization is performed after the method is invoked, but before the method returns to the caller.
+If that authorization denies access, the value is not returned, and an `AccessDeniedException` is thrown
+====
+
+To recreate what adding `@EnableMethodSecurity` does by default, you would publish the following configuration:
+
+.Full Pre-post Method Security Configuration
+====
+.Java
+[source,java,role="primary"]
+----
+@EnableMethodSecurity(prePostEnabled = false)
+class MethodSecurityConfig {
+ @Bean
+ @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+ Advisor preFilterAuthorizationMethodInterceptor() {
+ return new PreFilterAuthorizationMethodInterceptor();
+ }
+
+ @Bean
+ @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+ Advisor preAuthorizeAuthorizationMethodInterceptor() {
+ return AuthorizationManagerBeforeMethodInterceptor.preAuthorize();
+ }
+
+ @Bean
+ @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+ Advisor postAuthorizeAuthorizationMethodInterceptor() {
+ return AuthorizationManagerAfterMethodInterceptor.postAuthorize();
+ }
+
+ @Bean
+ @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+ Advisor postFilterAuthorizationMethodInterceptor() {
+ return new PostFilterAuthorizationMethodInterceptor();
+ }
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@EnableMethodSecurity(prePostEnabled = false)
+class MethodSecurityConfig {
+ @Bean
+ @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+ fun preFilterAuthorizationMethodInterceptor() : Advisor {
+ return PreFilterAuthorizationMethodInterceptor();
+ }
+
+ @Bean
+ @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+ fun preAuthorizeAuthorizationMethodInterceptor() : Advisor {
+ return AuthorizationManagerBeforeMethodInterceptor.preAuthorize();
+ }
+
+ @Bean
+ @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+ fun postAuthorizeAuthorizationMethodInterceptor() : Advisor {
+ return AuthorizationManagerAfterMethodInterceptor.postAuthorize();
+ }
+
+ @Bean
+ @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+ fun postFilterAuthorizationMethodInterceptor() : Advisor {
+ return PostFilterAuthorizationMethodInterceptor();
+ }
+}
+----
+
+.Xml
+[source,xml,role="secondary"]
+----
+
+
+
+
+
+
+
+
+----
+====
+
+Notice that Spring Security's method security is built using Spring AOP.
+So, interceptors are invoked based on the order specified.
+This can be customized by calling `setOrder` on the interceptor instances like so:
+
+.Publish Custom Advisor
+====
+.Java
+[source,java,role="primary"]
+----
+@Bean
+@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+Advisor postFilterAuthorizationMethodInterceptor() {
+ PostFilterAuthorizationMethodInterceptor interceptor = new PostFilterAuthorizationMethodInterceptor();
+ interceptor.setOrder(AuthorizationInterceptorOrders.POST_AUTHORIZE.getOrder() - 1);
+ return interceptor;
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@Bean
+@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+fun postFilterAuthorizationMethodInterceptor() : Advisor {
+ val interceptor = PostFilterAuthorizationMethodInterceptor();
+ interceptor.setOrder(AuthorizationInterceptorOrders.POST_AUTHORIZE.getOrder() - 1);
+ return interceptor;
+}
+----
+
+.Xml
+[source,xml,role="secondary"]
+----
+
+
+
+----
+====
+
+You may want to only support `@PreAuthorize` in your application, in which case you can do the following:
+
+
+.Only @PreAuthorize Configuration
+====
+.Java
+[source,java,role="primary"]
+----
+@EnableMethodSecurity(prePostEnabled = false)
+class MethodSecurityConfig {
+ @Bean
+ @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+ Advisor preAuthorize() {
+ return AuthorizationManagerBeforeMethodInterceptor.preAuthorize();
+ }
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@EnableMethodSecurity(prePostEnabled = false)
+class MethodSecurityConfig {
+ @Bean
+ @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+ fun preAuthorize() : Advisor {
+ return AuthorizationManagerBeforeMethodInterceptor.preAuthorize()
+ }
+}
+----
+
+.Xml
+[source,xml,role="secondary"]
+----
+
+
+
+
+
+----
+====
+
+Or, you may have a custom before-method `AuthorizationManager` that you want to add to the list.
+
+In this case, you will need to tell Spring Security both the `AuthorizationManager` and to which methods and classes your authorization manager applies.
+
+Thus, you can configure Spring Security to invoke your `AuthorizationManager` in between `@PreAuthorize` and `@PostAuthorize` like so:
+
+.Custom Before Advisor
+====
+
+.Java
+[source,java,role="primary"]
+----
+@EnableMethodSecurity
+class MethodSecurityConfig {
+ @Bean
+ @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+ public Advisor customAuthorize() {
+ JdkRegexpMethodPointcut pattern = new JdkRegexpMethodPointcut();
+ pattern.setPattern("org.mycompany.myapp.service.*");
+ AuthorizationManager rule = AuthorityAuthorizationManager.isAuthenticated();
+ AuthorizationManagerBeforeMethodInterceptor interceptor = new AuthorizationManagerBeforeMethodInterceptor(pattern, rule);
+ interceptor.setOrder(AuthorizationInterceptorsOrder.PRE_AUTHORIZE_ADVISOR_ORDER.getOrder() + 1);
+ return interceptor;
+ }
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@EnableMethodSecurity
+class MethodSecurityConfig {
+ @Bean
+ @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+ fun customAuthorize() : Advisor {
+ val pattern = JdkRegexpMethodPointcut();
+ pattern.setPattern("org.mycompany.myapp.service.*");
+ val rule = AuthorityAuthorizationManager.isAuthenticated();
+ val interceptor = AuthorizationManagerBeforeMethodInterceptor(pattern, rule);
+ interceptor.setOrder(AuthorizationInterceptorsOrder.PRE_AUTHORIZE_ADVISOR_ORDER.getOrder() + 1);
+ return interceptor;
+ }
+}
+----
+
+.Xml
+[source,xml,role="secondary"]
+----
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+----
+====
+
+[TIP]
+====
+You can place your interceptor in between Spring Security method interceptors using the order constants specified in `AuthorizationInterceptorsOrder`.
+====
+
+The same can be done for after-method authorization.
+After-method authorization is generally concerned with analysing the return value to verify access.
+
+For example, you might have a method that confirms that the account requested actually belongs to the logged-in user like so:
+
+.@PostAuthorize example
+====
+.Java
+[source,java,role="primary"]
+----
+public interface BankService {
+
+ @PreAuthorize("hasRole('USER')")
+ @PostAuthorize("returnObject.owner == authentication.name")
+ Account readAccount(Long id);
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+interface BankService {
+
+ @PreAuthorize("hasRole('USER')")
+ @PostAuthorize("returnObject.owner == authentication.name")
+ fun readAccount(id : Long) : Account
+}
+----
+====
+
+You can supply your own `AuthorizationMethodInterceptor` to customize how access to the return value is evaluated.
+
+For example, if you have your own custom annotation, you can configure it like so:
+
+
+.Custom After Advisor
+====
+.Java
+[source,java,role="primary"]
+----
+@EnableMethodSecurity
+class MethodSecurityConfig {
+ @Bean
+ @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+ public Advisor customAuthorize(AuthorizationManager rules) {
+ AnnotationMethodMatcher pattern = new AnnotationMethodMatcher(MySecurityAnnotation.class);
+ AuthorizationManagerAfterMethodInterceptor interceptor = new AuthorizationManagerAfterMethodInterceptor(pattern, rules);
+ interceptor.setOrder(AuthorizationInterceptorsOrder.POST_AUTHORIZE_ADVISOR_ORDER.getOrder() + 1);
+ return interceptor;
+ }
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@EnableMethodSecurity
+class MethodSecurityConfig {
+ @Bean
+ @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+ fun customAuthorize(rules : AuthorizationManager) : Advisor {
+ val pattern = AnnotationMethodMatcher(MySecurityAnnotation::class.java);
+ val interceptor = AuthorizationManagerAfterMethodInterceptor(pattern, rules);
+ interceptor.setOrder(AuthorizationInterceptorsOrder.POST_AUTHORIZE_ADVISOR_ORDER.getOrder() + 1);
+ return interceptor;
+ }
+}
+----
+
+.Xml
+[source,xml,role="secondary"]
+----
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+----
+====
+
+and it will be invoked after the `@PostAuthorize` interceptor.
+
+[[jc-enable-global-method-security]]
=== EnableGlobalMethodSecurity
We can enable annotation-based security using the `@EnableGlobalMethodSecurity` annotation on any `@Configuration` instance.