From b6cfd4fa76cb39838fccec4c963d73dbb28b93b2 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Wed, 11 Mar 2015 00:32:47 +0200 Subject: [PATCH] INT-3663: Fix Early BF Access for Security Module JIRA: https://jira.spring.io/browse/INT-3663 Previously the `ChannelSecurityInterceptorBeanPostProcessor` was populated with direct `BeanDefinition`s for `ChannelSecurityInterceptor`s. It caused an `early access to BeanFactory`. The issue has been introduced by the `ChannelSecurityInterceptorFactoryBean` * Rework `SecurityIntegrationConfigurationInitializer` do not populate `BeanDefinition`s to the `ChannelSecurityInterceptorBeanPostProcessor`, but just `bean names` * Redesign `ChannelSecurityInterceptorBeanPostProcessor` to the `AbstractAutoProxyCreator` * Introduce `SecuredChannel` annotation to be used on the `@Bean` level for `MessageChannel` definition * Move `access policy` mapping to the `SecuredChannel` annotation Address PR comments Document `@SecuredChannel` annotation --- .../channel/ChannelSecurityInterceptor.java | 7 +- .../channel/DefaultChannelAccessPolicy.java | 45 ++++++-- .../security/channel/SecuredChannel.java | 48 +++++++++ ...lSecurityInterceptorBeanPostProcessor.java | 82 +++++++++----- ...ChannelSecurityInterceptorFactoryBean.java | 3 + ...tyIntegrationConfigurationInitializer.java | 101 ++++++++++++------ ...rityInterceptorBeanPostProcessorTests.java | 74 ------------- ...rceptorSecuredChannelAnnotationTests.java} | 19 +++- src/reference/asciidoc/security.adoc | 46 ++++---- src/reference/asciidoc/whats-new.adoc | 7 ++ 10 files changed, 263 insertions(+), 169 deletions(-) create mode 100644 spring-integration-security/src/main/java/org/springframework/integration/security/channel/SecuredChannel.java delete mode 100644 spring-integration-security/src/test/java/org/springframework/integration/security/channel/ChannelSecurityInterceptorBeanPostProcessorTests.java rename spring-integration-security/src/test/java/org/springframework/integration/security/config/{ChannelSecurityInterceptorFactoryBeanTests.java => ChannelSecurityInterceptorSecuredChannelAnnotationTests.java} (82%) diff --git a/spring-integration-security/src/main/java/org/springframework/integration/security/channel/ChannelSecurityInterceptor.java b/spring-integration-security/src/main/java/org/springframework/integration/security/channel/ChannelSecurityInterceptor.java index 0e9b4e6317..a82fd17bb1 100644 --- a/spring-integration-security/src/main/java/org/springframework/integration/security/channel/ChannelSecurityInterceptor.java +++ b/spring-integration-security/src/main/java/org/springframework/integration/security/channel/ChannelSecurityInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -30,11 +30,15 @@ import org.springframework.util.Assert; * * @author Mark Fisher * @author Oleg Zhurakousky + * @see SecuredChannel */ public final class ChannelSecurityInterceptor extends AbstractSecurityInterceptor implements MethodInterceptor { private final ChannelSecurityMetadataSource securityMetadataSource; + public ChannelSecurityInterceptor() { + this(new ChannelSecurityMetadataSource()); + } public ChannelSecurityInterceptor(ChannelSecurityMetadataSource securityMetadataSource) { Assert.notNull(securityMetadataSource, "securityMetadataSource must not be null"); @@ -68,7 +72,6 @@ public final class ChannelSecurityInterceptor extends AbstractSecurityIntercepto return returnValue; } - @Override public SecurityMetadataSource obtainSecurityMetadataSource() { return this.securityMetadataSource; diff --git a/spring-integration-security/src/main/java/org/springframework/integration/security/channel/DefaultChannelAccessPolicy.java b/spring-integration-security/src/main/java/org/springframework/integration/security/channel/DefaultChannelAccessPolicy.java index 607ca0cf55..4ef227cee0 100644 --- a/spring-integration-security/src/main/java/org/springframework/integration/security/channel/DefaultChannelAccessPolicy.java +++ b/spring-integration-security/src/main/java/org/springframework/integration/security/channel/DefaultChannelAccessPolicy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import java.util.HashSet; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.SecurityConfig; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** @@ -31,6 +32,7 @@ import org.springframework.util.StringUtils; * * @author Mark Fisher * @author Oleg Zhurakousky + * @author Artem Bilan */ public class DefaultChannelAccessPolicy implements ChannelAccessPolicy { @@ -44,16 +46,14 @@ public class DefaultChannelAccessPolicy implements ChannelAccessPolicy { * values may be a single String or a comma-delimited list of values. All whitespace * will be trimmed. A null value indicates that the policy does not * apply for either send or receive access type. At most one of the values may be null. - * * @param sendAccess The send access value(s). * @param receiveAccess The receive access value(s). */ - @SuppressWarnings("unchecked") public DefaultChannelAccessPolicy(String sendAccess, String receiveAccess) { boolean sendAccessDefined = StringUtils.hasText(sendAccess); boolean receiveAccessDefined = StringUtils.hasText(receiveAccess); Assert.isTrue(sendAccessDefined || receiveAccessDefined, - "At least one of 'sendAccess' and 'receiveAccess' must not be null."); + "At least one of 'sendAccess' and 'receiveAccess' must not be null and have at least one entry."); if (sendAccessDefined) { String[] sendAccessValues = StringUtils.commaDelimitedListToStringArray(sendAccess); configAttributeDefinitionForSend = new HashSet(); @@ -62,7 +62,7 @@ public class DefaultChannelAccessPolicy implements ChannelAccessPolicy { } } else { - configAttributeDefinitionForSend = Collections.EMPTY_SET; + configAttributeDefinitionForSend = Collections.emptySet(); } if (receiveAccessDefined) { String[] receiveAccessValues = StringUtils.commaDelimitedListToStringArray(receiveAccess); @@ -72,7 +72,40 @@ public class DefaultChannelAccessPolicy implements ChannelAccessPolicy { } } else { - configAttributeDefinitionForReceive = Collections.EMPTY_SET; + configAttributeDefinitionForReceive = Collections.emptySet(); + } + } + + /** + * Create an access policy instance. A null value indicates that the policy does not + * apply for either send or receive access type. At most one of the values may be null. + * Typically is used for the values from the {@link SecuredChannel} + * @param sendAccess The send access values. + * @param receiveAccess The receive access values. + * @since 4.2 + */ + public DefaultChannelAccessPolicy(String[] sendAccess, String[] receiveAccess) { + boolean sendAccessDefined = !ObjectUtils.isEmpty(sendAccess); + boolean receiveAccessDefined = !ObjectUtils.isEmpty(receiveAccess); + Assert.isTrue(sendAccessDefined || receiveAccessDefined, + "At least one of 'sendAccess' and 'receiveAccess' must not be null."); + if (sendAccessDefined) { + configAttributeDefinitionForSend = new HashSet(); + for (String sendAccessValue : sendAccess) { + configAttributeDefinitionForSend.add(new SecurityConfig(sendAccessValue)); + } + } + else { + configAttributeDefinitionForSend = Collections.emptySet(); + } + if (receiveAccessDefined) { + configAttributeDefinitionForReceive = new HashSet(); + for (String receiveAccessValue : receiveAccess) { + configAttributeDefinitionForReceive.add(new SecurityConfig(receiveAccessValue)); + } + } + else { + configAttributeDefinitionForReceive = Collections.emptySet(); } } diff --git a/spring-integration-security/src/main/java/org/springframework/integration/security/channel/SecuredChannel.java b/spring-integration-security/src/main/java/org/springframework/integration/security/channel/SecuredChannel.java new file mode 100644 index 0000000000..4a9c551cb4 --- /dev/null +++ b/spring-integration-security/src/main/java/org/springframework/integration/security/channel/SecuredChannel.java @@ -0,0 +1,48 @@ +/* + * Copyright 2015 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 + * + * http://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.integration.security.channel; + +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; + +/** + * An annotation to be applied for the {@link org.springframework.messaging.MessageChannel} bean definition + * from JavaConfig - on {@link @Bean} method level. + *

+ * Applies the {@link ChannelSecurityInterceptor}(s) using provided {@link #interceptor()} bean name(s). + *

+ * The {@link #sendAccess()} and {@link #receiveAccess()} policies are populated to the + * {@link ChannelSecurityInterceptor}(s) from the {@code ChannelSecurityInterceptorBeanPostProcessor}. + * + * @author Artem Bilan + * @since 4.2 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface SecuredChannel { + + String[] interceptor(); + + String[] sendAccess() default {}; + + String[] receiveAccess() default {}; + +} diff --git a/spring-integration-security/src/main/java/org/springframework/integration/security/config/ChannelSecurityInterceptorBeanPostProcessor.java b/spring-integration-security/src/main/java/org/springframework/integration/security/config/ChannelSecurityInterceptorBeanPostProcessor.java index 1e590d5d4e..2d77084777 100644 --- a/spring-integration-security/src/main/java/org/springframework/integration/security/config/ChannelSecurityInterceptorBeanPostProcessor.java +++ b/spring-integration-security/src/main/java/org/springframework/integration/security/config/ChannelSecurityInterceptorBeanPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,16 +16,19 @@ package org.springframework.integration.security.config; -import java.util.Collection; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; import java.util.Set; import java.util.regex.Pattern; -import org.springframework.aop.framework.Advised; -import org.springframework.aop.framework.ProxyFactory; -import org.springframework.aop.support.AopUtils; -import org.springframework.aop.support.DefaultPointcutAdvisor; +import org.springframework.aop.Advisor; +import org.springframework.aop.TargetSource; +import org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator; +import org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.integration.security.channel.ChannelAccessPolicy; import org.springframework.integration.security.channel.ChannelSecurityInterceptor; import org.springframework.integration.security.channel.ChannelSecurityMetadataSource; import org.springframework.messaging.MessageChannel; @@ -37,40 +40,61 @@ import org.springframework.messaging.MessageChannel; * @author Oleg Zhurakousky * @author Artem Bilan */ -public class ChannelSecurityInterceptorBeanPostProcessor implements BeanPostProcessor { +@SuppressWarnings("serial") +public class ChannelSecurityInterceptorBeanPostProcessor extends AbstractAutoProxyCreator { - private final Collection securityInterceptors; + private final Map> securityInterceptorMappings; - public ChannelSecurityInterceptorBeanPostProcessor(Collection securityInterceptors) { - this.securityInterceptors = securityInterceptors; + private final Map> accessPolicyMapping; + + public ChannelSecurityInterceptorBeanPostProcessor(Map> securityInterceptorMappings) { + this(securityInterceptorMappings, null); } - public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { - return bean; + public ChannelSecurityInterceptorBeanPostProcessor(Map> securityInterceptorMappings, + Map> accessPolicyMapping) { + this.securityInterceptorMappings = securityInterceptorMappings;//NOSONAR (inconsistent sync) + this.accessPolicyMapping = accessPolicyMapping;//NOSONAR (inconsistent sync) } - public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { - if (bean instanceof MessageChannel) { - for (ChannelSecurityInterceptor securityInterceptor : this.securityInterceptors) { - ChannelSecurityMetadataSource channelSecurityMetadataSource = - (ChannelSecurityMetadataSource) securityInterceptor.obtainSecurityMetadataSource(); - if (this.shouldProxy(beanName, channelSecurityMetadataSource)) { - if (AopUtils.isAopProxy(bean) && bean instanceof Advised) { - ((Advised) bean).addAdvisor(new DefaultPointcutAdvisor(securityInterceptor)); - } - else { - ProxyFactory proxyFactory = new ProxyFactory(bean); - proxyFactory.addAdvisor(new DefaultPointcutAdvisor(securityInterceptor)); - bean = proxyFactory.getProxy(); - } - } + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) { + if (this.accessPolicyMapping != null + && bean instanceof ChannelSecurityInterceptor + && accessPolicyMapping.containsKey(beanName)) { + Map accessPolicies = this.accessPolicyMapping.get(beanName); + ChannelSecurityMetadataSource securityMetadataSource = + (ChannelSecurityMetadataSource) ((ChannelSecurityInterceptor) bean).obtainSecurityMetadataSource(); + for (Map.Entry entry : accessPolicies.entrySet()) { + securityMetadataSource.addPatternMapping(entry.getKey(), entry.getValue()); } } return bean; } - private boolean shouldProxy(String beanName, ChannelSecurityMetadataSource channelSecurityMetadataSource) { - Set patterns = channelSecurityMetadataSource.getPatterns(); + @Override + protected Object[] getAdvicesAndAdvisorsForBean(Class beanClass, String beanName, + TargetSource customTargetSource) throws BeansException { + if (MessageChannel.class.isAssignableFrom(beanClass)) { + List interceptors = new ArrayList(); + for (Map.Entry> entry : this.securityInterceptorMappings.entrySet()) { + if (isMatch(beanName, entry.getValue())) { + DefaultBeanFactoryPointcutAdvisor channelSecurityInterceptor + = new DefaultBeanFactoryPointcutAdvisor(); + channelSecurityInterceptor.setAdviceBeanName(entry.getKey()); + channelSecurityInterceptor.setBeanFactory(getBeanFactory()); + interceptors.add(channelSecurityInterceptor); + } + } + if (!interceptors.isEmpty()) { + return interceptors.toArray(); + } + } + + return DO_NOT_PROXY; + } + + private boolean isMatch(String beanName, Set patterns) { for (Pattern pattern : patterns) { if (pattern.matcher(beanName).matches()) { return true; diff --git a/spring-integration-security/src/main/java/org/springframework/integration/security/config/ChannelSecurityInterceptorFactoryBean.java b/spring-integration-security/src/main/java/org/springframework/integration/security/config/ChannelSecurityInterceptorFactoryBean.java index 2ed5d438f7..f350b4cac3 100644 --- a/spring-integration-security/src/main/java/org/springframework/integration/security/config/ChannelSecurityInterceptorFactoryBean.java +++ b/spring-integration-security/src/main/java/org/springframework/integration/security/config/ChannelSecurityInterceptorFactoryBean.java @@ -40,7 +40,10 @@ import org.springframework.util.Assert; * * @author Artem Bilan * @since 4.0 + * @deprecated in favor of direct {@link ChannelSecurityInterceptor} usage and + * {@link org.springframework.integration.security.channel.SecuredChannel} annotation. */ +@Deprecated public class ChannelSecurityInterceptorFactoryBean implements FactoryBean, BeanNameAware, BeanFactoryAware { private final ChannelSecurityInterceptor interceptor = new ChannelSecurityInterceptor(new ChannelSecurityMetadataSource()); diff --git a/spring-integration-security/src/main/java/org/springframework/integration/security/config/SecurityIntegrationConfigurationInitializer.java b/spring-integration-security/src/main/java/org/springframework/integration/security/config/SecurityIntegrationConfigurationInitializer.java index aa3491bd7d..bb8717b88f 100644 --- a/spring-integration-security/src/main/java/org/springframework/integration/security/config/SecurityIntegrationConfigurationInitializer.java +++ b/spring-integration-security/src/main/java/org/springframework/integration/security/config/SecurityIntegrationConfigurationInitializer.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,23 +16,24 @@ package org.springframework.integration.security.config; -import java.lang.reflect.Method; -import java.util.List; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Pattern; import org.springframework.beans.BeansException; -import org.springframework.beans.factory.CannotLoadBeanClassException; import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.beans.factory.support.ManagedList; +import org.springframework.beans.factory.support.ManagedMap; +import org.springframework.beans.factory.support.ManagedSet; import org.springframework.core.type.MethodMetadata; -import org.springframework.core.type.StandardMethodMetadata; import org.springframework.integration.config.IntegrationConfigurationInitializer; +import org.springframework.integration.security.channel.ChannelAccessPolicy; import org.springframework.integration.security.channel.ChannelSecurityInterceptor; -import org.springframework.util.ClassUtils; -import org.springframework.util.StringUtils; +import org.springframework.integration.security.channel.DefaultChannelAccessPolicy; +import org.springframework.integration.security.channel.SecuredChannel; /** * The Integration Security infrastructure {@code beanFactory} initializer. @@ -46,45 +47,75 @@ public class SecurityIntegrationConfigurationInitializer implements IntegrationC ChannelSecurityInterceptorBeanPostProcessor.class.getName(); @Override + @SuppressWarnings("unchecked") public void initialize(ConfigurableListableBeanFactory beanFactory) throws BeansException { BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory; - List securityInterceptors = new ManagedList(); + Map> securityInterceptors = new ManagedMap>(); + Map> policies = new HashMap>(); for (String beanName : registry.getBeanDefinitionNames()) { BeanDefinition beanDefinition = registry.getBeanDefinition(beanName); - String beanClassName = beanDefinition.getBeanClassName(); - Class clazz = null; - if (StringUtils.hasText(beanClassName)) { - try { - clazz = ClassUtils.forName(beanClassName, beanFactory.getBeanClassLoader()); - } - catch (ClassNotFoundException e) { - throw new CannotLoadBeanClassException(this.toString(), beanName, beanClassName, e); - } - } - else if (beanDefinition instanceof AnnotatedBeanDefinition - && beanDefinition.getSource() instanceof MethodMetadata) { - MethodMetadata beanMethod = (MethodMetadata) beanDefinition.getSource(); - if (beanMethod instanceof StandardMethodMetadata) { - Method method = ((StandardMethodMetadata) beanMethod).getIntrospectedMethod(); - clazz = method.getReturnType(); - } - } + if (ChannelSecurityInterceptor.class.getName().equals(beanDefinition.getBeanClassName())) { + BeanDefinition metadataSource = (BeanDefinition) beanDefinition.getConstructorArgumentValues() + .getIndexedArgumentValue(0, BeanDefinition.class) + .getValue(); - if (clazz != null && - (ChannelSecurityInterceptor.class.isAssignableFrom(clazz) - || ChannelSecurityInterceptorFactoryBean.class.isAssignableFrom(clazz))) { - securityInterceptors.add(beanDefinition); + Map value = (Map) metadataSource.getConstructorArgumentValues() + .getIndexedArgumentValue(0, Map.class) + .getValue(); + ManagedSet patterns = new ManagedSet(); + if (!securityInterceptors.containsKey(beanName)) { + securityInterceptors.put(beanName, patterns); + } + else { + patterns = securityInterceptors.get(beanName); + } + patterns.addAll(value.keySet()); + } + else if (beanDefinition instanceof AnnotatedBeanDefinition) { + if (beanDefinition.getSource() instanceof MethodMetadata) { + MethodMetadata beanMethod = (MethodMetadata) beanDefinition.getSource(); + String annotationType = SecuredChannel.class.getName(); + if (beanMethod.isAnnotated(annotationType)) { + Map securedAttributes = beanMethod.getAnnotationAttributes(annotationType); + String[] interceptors = (String[]) securedAttributes.get("interceptor"); + String[] sendAccess = (String[]) securedAttributes.get("sendAccess"); + String[] receiveAccess = (String[]) securedAttributes.get("receiveAccess"); + ChannelAccessPolicy accessPolicy = new DefaultChannelAccessPolicy(sendAccess, receiveAccess); + for (String interceptor : interceptors) { + ManagedSet patterns = new ManagedSet(); + if (!securityInterceptors.containsKey(interceptor)) { + securityInterceptors.put(interceptor, patterns); + } + else { + patterns = securityInterceptors.get(interceptor); + } + patterns.add(beanName); + + Map mapping = new HashMap(); + if (!policies.containsKey(interceptor)) { + policies.put(interceptor, mapping); + } + else { + mapping = policies.get(interceptor); + } + mapping.put(Pattern.compile(beanName), accessPolicy); + } + } + } } } if (!securityInterceptors.isEmpty()) { - BeanDefinition securityPostProcessorBd = + + BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(ChannelSecurityInterceptorBeanPostProcessor.class) - .addConstructorArgValue(securityInterceptors) - .getBeanDefinition(); - registry.registerBeanDefinition(CHANNEL_SECURITY_INTERCEPTOR_BPP_BEAN_NAME, securityPostProcessorBd); + .addConstructorArgValue(securityInterceptors); + if (!policies.isEmpty()) { + builder.addConstructorArgValue(policies); + } + registry.registerBeanDefinition(CHANNEL_SECURITY_INTERCEPTOR_BPP_BEAN_NAME, builder.getBeanDefinition()); } } diff --git a/spring-integration-security/src/test/java/org/springframework/integration/security/channel/ChannelSecurityInterceptorBeanPostProcessorTests.java b/spring-integration-security/src/test/java/org/springframework/integration/security/channel/ChannelSecurityInterceptorBeanPostProcessorTests.java deleted file mode 100644 index e5c744e0af..0000000000 --- a/spring-integration-security/src/test/java/org/springframework/integration/security/channel/ChannelSecurityInterceptorBeanPostProcessorTests.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2002-2014 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 - * - * http://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.integration.security.channel; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -import java.util.Arrays; -import java.util.regex.Pattern; - -import org.junit.Test; - -import org.springframework.aop.support.AopUtils; -import org.springframework.integration.channel.QueueChannel; -import org.springframework.integration.security.config.ChannelSecurityInterceptorBeanPostProcessor; -import org.springframework.messaging.MessageChannel; - -/** - * @author Mark Fisher - * @author Artem Bilan - */ -public class ChannelSecurityInterceptorBeanPostProcessorTests { - - @Test - public void securedChannelIsProxied() throws Exception { - ChannelSecurityMetadataSource securityMetadataSource = new ChannelSecurityMetadataSource(); - securityMetadataSource.addPatternMapping(Pattern.compile("secured.*"), - new DefaultChannelAccessPolicy("ROLE_ADMIN", null)); - - ChannelSecurityInterceptor interceptor = new ChannelSecurityInterceptor(securityMetadataSource); - - ChannelSecurityInterceptorBeanPostProcessor postProcessor = - new ChannelSecurityInterceptorBeanPostProcessor(Arrays.asList(interceptor)); - - QueueChannel securedChannel = new QueueChannel(); - securedChannel.setBeanName("securedChannel"); - MessageChannel postProcessedChannel = - (MessageChannel) postProcessor.postProcessAfterInitialization(securedChannel, "securedChannel"); - assertTrue(AopUtils.isAopProxy(postProcessedChannel)); - } - - @Test - public void nonsecuredChannelIsNotProxied() throws Exception { - ChannelSecurityMetadataSource securityMetadataSource = new ChannelSecurityMetadataSource(); - securityMetadataSource.addPatternMapping(Pattern.compile("secured.*"), - new DefaultChannelAccessPolicy("ROLE_ADMIN", null)); - - ChannelSecurityInterceptor interceptor = new ChannelSecurityInterceptor(securityMetadataSource); - - ChannelSecurityInterceptorBeanPostProcessor postProcessor = - new ChannelSecurityInterceptorBeanPostProcessor(Arrays.asList(interceptor)); - - QueueChannel channel = new QueueChannel(); - channel.setBeanName("testChannel"); - MessageChannel postProcessedChannel = - (MessageChannel) postProcessor.postProcessAfterInitialization(channel, "testChannel"); - assertFalse(AopUtils.isAopProxy(postProcessedChannel)); - } - -} diff --git a/spring-integration-security/src/test/java/org/springframework/integration/security/config/ChannelSecurityInterceptorFactoryBeanTests.java b/spring-integration-security/src/test/java/org/springframework/integration/security/config/ChannelSecurityInterceptorSecuredChannelAnnotationTests.java similarity index 82% rename from spring-integration-security/src/test/java/org/springframework/integration/security/config/ChannelSecurityInterceptorFactoryBeanTests.java rename to spring-integration-security/src/test/java/org/springframework/integration/security/config/ChannelSecurityInterceptorSecuredChannelAnnotationTests.java index 96d8f91488..f2b4ee41df 100644 --- a/spring-integration-security/src/test/java/org/springframework/integration/security/config/ChannelSecurityInterceptorFactoryBeanTests.java +++ b/spring-integration-security/src/test/java/org/springframework/integration/security/config/ChannelSecurityInterceptorSecuredChannelAnnotationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-2015 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. @@ -30,10 +30,14 @@ import org.springframework.integration.channel.DirectChannel; import org.springframework.integration.config.EnableIntegration; import org.springframework.integration.security.SecurityTestUtils; import org.springframework.integration.security.TestHandler; +import org.springframework.integration.security.channel.ChannelSecurityInterceptor; +import org.springframework.integration.security.channel.SecuredChannel; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.SubscribableChannel; import org.springframework.messaging.support.GenericMessage; +import org.springframework.security.access.AccessDecisionManager; import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; @@ -48,7 +52,7 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) -public class ChannelSecurityInterceptorFactoryBeanTests { +public class ChannelSecurityInterceptorSecuredChannelAnnotationTests { @Autowired MessageChannel securedChannel; @@ -133,11 +137,13 @@ public class ChannelSecurityInterceptorFactoryBeanTests { public static class ContextConfiguration { @Bean + @SecuredChannel(interceptor = "channelSecurityInterceptor", sendAccess = {"ROLE_ADMIN", "ROLE_PRESIDENT"}) public SubscribableChannel securedChannel() { return new DirectChannel(); } @Bean + @SecuredChannel(interceptor = "channelSecurityInterceptor", sendAccess = {"ROLE_ADMIN", "ROLE_PRESIDENT"}) public SubscribableChannel securedChannel2() { return new DirectChannel(); } @@ -157,9 +163,12 @@ public class ChannelSecurityInterceptorFactoryBeanTests { } @Bean - public ChannelSecurityInterceptorFactoryBean channelSecurityInterceptor() { - return new ChannelSecurityInterceptorFactoryBean() - .accessPolicy("securedChannel.*", "ROLE_ADMIN, ROLE_PRESIDENT"); + public ChannelSecurityInterceptor channelSecurityInterceptor(AuthenticationManager authenticationManager, + AccessDecisionManager accessDecisionManager) { + ChannelSecurityInterceptor channelSecurityInterceptor = new ChannelSecurityInterceptor(); + channelSecurityInterceptor.setAuthenticationManager(authenticationManager); + channelSecurityInterceptor.setAccessDecisionManager(accessDecisionManager); + return channelSecurityInterceptor; } } diff --git a/src/reference/asciidoc/security.adoc b/src/reference/asciidoc/security.adoc index e93e049d6f..73ac688d86 100644 --- a/src/reference/asciidoc/security.adoc +++ b/src/reference/asciidoc/security.adoc @@ -55,8 +55,14 @@ Where this is not the case references to the appropriate beans can be configured ---- -Starting with _version 4.0_, the same configuration is available when using `@Configuration` classes, by declaring a `ChannelSecurityInterceptorFactoryBean`. -This class delegates all options for the `ChannelSecurityInterceptor` with a _builder_ pattern: +Starting with _version 4.2_, the `@SecuredChannel` annotation is available, replacing the deprecated +`ChannelSecurityInterceptorFactoryBean`, which was introduced in _version 4.0_ for Java & Annotation +configuration in `@Configuration` classes. +The `ChannelSecurityInterceptorFactoryBean` has been deprecated to +avoid the possibility of undesired early load for dependent beans from the `BeanFactory` during the `ApplicationContext` initialization +phase. + +With the `@SecuredChannel` annotation, the Java configuration variant of the XML configuration above is: [source,java] ---- @@ -64,22 +70,26 @@ This class delegates all options for the `ChannelSecurityInterceptor` with a _bu @EnableIntegration public class ContextConfiguration { - @Autowired - private AuthenticationManager authenticationManager; - - @Autowired - private AccessDecisionManager accessDecisionManager; - @Bean - public ChannelSecurityInterceptorFactoryBean channelSecurityInterceptor() { - return new ChannelSecurityInterceptorFactoryBean() - .authenticationManager(this.authenticationManager) - .accessDecisionManager(this.accessDecisionManager) - .accessPolicy("admin.*", "ROLE_ADMIN") - .accessPolicy("user.*", null, "ROLE_USER"); - } + @SecuredChannel(interceptor = "channelSecurityInterceptor", sendAccess = "ROLE_ADMIN") + public SubscribableChannel adminChannel() { + return new DirectChannel(); + } + + @Bean + @SecuredChannel(interceptor = "channelSecurityInterceptor", receiveAccess = "ROLE_USER") + public SubscribableChannel userChannel() { + return new DirectChannel(); + } + + @Bean + public ChannelSecurityInterceptor channelSecurityInterceptor(AuthenticationManager authenticationManager, + AccessDecisionManager accessDecisionManager) { + ChannelSecurityInterceptor channelSecurityInterceptor = new ChannelSecurityInterceptor(); + channelSecurityInterceptor.setAuthenticationManager(authenticationManager); + channelSecurityInterceptor.setAccessDecisionManager(accessDecisionManager); + return channelSecurityInterceptor; + } + } ---- - -NOTE: The `@EnableIntegration` annotation is required to provide the Spring Integration infrastructure (including Security) to the Application Context. -In addition this `FactoryBean` falls back to `AuthenticationManager` and `AccessDecisionManager` beans with names `authenticationManager` and `accessDecisionManager` respectively, if they aren't provided in the `ChannelSecurityInterceptorFactoryBean` bean definition. diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index 31acafc5f7..c6dc75a273 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -22,6 +22,13 @@ For complete details, see <>. The `MongoDbMetadataStore` is now available. For more information, see <>. +[[x4.2-secured-channel-annotation]] +==== SecuredChannel Annotation + +The `@SecuredChannel` annotation has been introduced, replacing the deprecated `ChannelSecurityInterceptorFactoryBean`. +For more information, see <>. + + [[x4.2-general]] === General Changes