GH-3897: Deprecate ChannelSecurityInterceptor (#3915)

* GH-3897: Deprecate `ChannelSecurityInterceptor`

Fixes https://github.com/spring-projects/spring-integration/issues/3897

Spring Security has deprecated `AccessDecisionManager` and all its infrastructure
in favor of `AuthorizationManager`

* Deprecate and AOP `ChannelSecurityInterceptor` and all its infrastructure,
including `@SecuredChannel` and respective XML configuration.
The `AuthorizationChannelInterceptor` added to respective channels for security
or configured as a global channel interceptor fully covers the previous AOP configuration
* Fix deprecation warnings in other tests with security

* Fix language in docs

Co-authored-by: Gary Russell <grussell@vmware.com>

* * Remove `forRemoval` attr from `@Deprecated` markers for Security classes:
looks like to mark `@Deprecated` and even `@SuppressWarnings("deprecation")`
don't silence warnings on compilation

Co-authored-by: Gary Russell <grussell@vmware.com>
This commit is contained in:
Artem Bilan
2022-10-17 18:32:17 -04:00
committed by GitHub
parent ff076b6a19
commit 64f0e738a9
26 changed files with 255 additions and 688 deletions

View File

@@ -169,6 +169,7 @@ allprojects {
mavenBom "org.apache.camel:camel-bom:$camelVersion"
mavenBom "org.testcontainers:testcontainers-bom:$testcontainersVersion"
mavenBom "org.apache.groovy:groovy-bom:$groovyVersion"
mavenBom "org.springframework.security:spring-security-bom:$springSecurityVersion"
mavenBom "org.jetbrains.kotlinx:kotlinx-coroutines-bom:$kotlinCoroutinesVersion"
}
@@ -675,10 +676,10 @@ project('spring-integration-http') {
testImplementation project(':spring-integration-security')
testImplementation "org.hamcrest:hamcrest-core:$hamcrestVersion"
testImplementation("org.springframework.security:spring-security-config:$springSecurityVersion") {
testImplementation('org.springframework.security:spring-security-config') {
exclude group: 'org.springframework'
}
testImplementation("org.springframework.security:spring-security-test:$springSecurityVersion") {
testImplementation('org.springframework.security:spring-security-test') {
exclude group: 'org.springframework'
}
testImplementation 'com.fasterxml.jackson.core:jackson-databind'
@@ -879,11 +880,11 @@ project('spring-integration-security') {
description = 'Spring Integration Security Support'
dependencies {
api project(':spring-integration-core')
api("org.springframework.security:spring-security-core:$springSecurityVersion") {
api('org.springframework.security:spring-security-messaging') {
exclude group: 'org.springframework'
}
testImplementation("org.springframework.security:spring-security-config:$springSecurityVersion") {
testImplementation('org.springframework.security:spring-security-config') {
exclude group: 'org.springframework'
}
}
@@ -971,10 +972,10 @@ project('spring-integration-webflux') {
testImplementation "jakarta.servlet:jakarta.servlet-api:$servletApiVersion"
testImplementation "org.hamcrest:hamcrest-core:$hamcrestVersion"
testImplementation 'org.springframework:spring-webmvc'
testImplementation("org.springframework.security:spring-security-config:$springSecurityVersion") {
testImplementation('org.springframework.security:spring-security-config') {
exclude group: 'org.springframework'
}
testImplementation("org.springframework.security:spring-security-test:$springSecurityVersion") {
testImplementation('org.springframework.security:spring-security-test') {
exclude group: 'org.springframework'
}
testImplementation 'com.fasterxml.jackson.core:jackson-databind'

View File

@@ -27,7 +27,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -52,22 +51,18 @@ import org.springframework.integration.dsl.context.IntegrationFlowContext;
import org.springframework.integration.handler.AbstractReplyProducingMessageHandler;
import org.springframework.integration.http.multipart.UploadedMultipartFile;
import org.springframework.integration.http.outbound.HttpRequestExecutingMessageHandler;
import org.springframework.integration.security.channel.ChannelSecurityInterceptor;
import org.springframework.integration.security.channel.SecuredChannel;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.PollableChannel;
import org.springframework.messaging.support.ErrorMessage;
import org.springframework.mock.web.MockPart;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.vote.AffirmativeBased;
import org.springframework.security.access.vote.RoleVoter;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.authorization.AuthorityAuthorizationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.messaging.access.intercept.AuthorizationChannelInterceptor;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@@ -335,9 +330,11 @@ public class HttpDslTests {
}
@Bean
@SecuredChannel(interceptor = "channelSecurityInterceptor", sendAccess = "ROLE_ADMIN")
public MessageChannel transformSecuredChannel() {
return new DirectChannel();
DirectChannel directChannel = new DirectChannel();
directChannel.addInterceptor(
new AuthorizationChannelInterceptor(AuthorityAuthorizationManager.hasRole("ADMIN")));
return directChannel;
}
@Bean
@@ -393,21 +390,6 @@ public class HttpDslTests {
return new StandardServletMultipartResolver();
}
@Bean
public AccessDecisionManager accessDecisionManager() {
return new AffirmativeBased(Collections.singletonList(new RoleVoter()));
}
@Bean
public ChannelSecurityInterceptor channelSecurityInterceptor(AccessDecisionManager accessDecisionManager,
AuthenticationManagerBuilder authenticationManagerBuilder) {
ChannelSecurityInterceptor channelSecurityInterceptor = new ChannelSecurityInterceptor();
channelSecurityInterceptor.setAuthenticationManager(authenticationManagerBuilder.getOrBuild());
channelSecurityInterceptor.setAccessDecisionManager(accessDecisionManager);
return channelSecurityInterceptor;
}
@Bean
public Validator customValidator() {
return new TestModelValidator();

View File

@@ -111,12 +111,6 @@ public class CookieTests {
return null;
}
@Override
@Deprecated
public String getMethodValue() {
return null;
}
public ClientHttpResponse execute() {
allHeaders.add(headers);
return new ClientHttpResponse() {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -26,7 +26,11 @@ import org.springframework.security.access.ConfigAttribute;
*
* @author Oleg Zhurakousky
* @since 2.0
*
* @deprecated since 6.0 in favor of literally
* {@code new AuthorizationChannelInterceptor(AuthorityAuthorizationManager.hasAnyRole())}
*/
@Deprecated(since = "6.0")
public interface ChannelAccessPolicy {
Collection<ConfigAttribute> getConfigAttributesForSend();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2022 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.
@@ -29,7 +29,11 @@ import org.springframework.util.Assert;
* is a <em>send</em> operation, the {@link Message} is also available.
*
* @author Mark Fisher
*
* @deprecated since 6.0 in favor of literally
* {@code new AuthorizationChannelInterceptor(AuthorityAuthorizationManager.hasAnyRole())}
*/
@Deprecated(since = "6.0")
public class ChannelInvocation {
private final MessageChannel channel;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2022 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.
@@ -31,8 +31,15 @@ import org.springframework.util.Assert;
*
* @author Mark Fisher
* @author Oleg Zhurakousky
*
* @see SecuredChannel
*
* @deprecated since 6.0 in favor of literally
* {@code new AuthorizationChannelInterceptor(AuthorityAuthorizationManager.hasAnyRole())}.
* However, the {@link org.springframework.security.messaging.access.intercept.AuthorizationChannelInterceptor}
* can be configured with any {@link org.springframework.security.authorization.AuthorizationManager} implementation.
*/
@Deprecated(since = "6.0")
public final class ChannelSecurityInterceptor extends AbstractSecurityInterceptor implements MethodInterceptor {
private final ChannelSecurityMetadataSource securityMetadataSource;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2022 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.
@@ -36,7 +36,11 @@ import org.springframework.util.Assert;
*
* @author Mark Fisher
* @author Oleg Zhurakousky
*
* @deprecated since 6.0 in favor of literally
* {@code new AuthorizationChannelInterceptor(AuthorityAuthorizationManager.hasAnyRole())}
*/
@Deprecated(since = "6.0")
public class ChannelSecurityMetadataSource implements SecurityMetadataSource {
private final Map<Pattern, ChannelAccessPolicy> patternMappings;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2022 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.
@@ -33,7 +33,11 @@ import org.springframework.util.StringUtils;
* @author Mark Fisher
* @author Oleg Zhurakousky
* @author Artem Bilan
*
* @deprecated since 6.0 in favor of literally
* {@code new AuthorizationChannelInterceptor(AuthorityAuthorizationManager.hasAnyRole())}
*/
@Deprecated(since = "6.0")
public class DefaultChannelAccessPolicy implements ChannelAccessPolicy {
private final Collection<ConfigAttribute> configAttributeDefinitionForSend;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015-2019 the original author or authors.
* Copyright 2015-2022 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.
@@ -33,7 +33,11 @@ import java.lang.annotation.Target;
*
* @author Artem Bilan
* @since 4.2
*
* @deprecated since 6.0 in favor of literally
* {@code new AuthorizationChannelInterceptor(AuthorityAuthorizationManager.hasAnyRole())}
*/
@Deprecated(since = "6.0")
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2022 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.
@@ -42,7 +42,11 @@ import org.springframework.messaging.MessageChannel;
* @author Oleg Zhurakousky
* @author Artem Bilan
* @author Gary Russell
* @deprecated since 6.0 in favor of literally
* {@code new AuthorizationChannelInterceptor(AuthorityAuthorizationManager.hasAnyRole())}
*/
@Deprecated(since = "6.0")
@SuppressWarnings("serial")
public class ChannelSecurityInterceptorBeanPostProcessor extends AbstractAutoProxyCreator {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2022 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.
@@ -22,7 +22,11 @@ import org.springframework.integration.config.xml.AbstractIntegrationNamespaceHa
* Namespace handler for the security namespace.
*
* @author Jonas Partner
*
* @deprecated since 6.0 in favor of literally
* {@code new AuthorizationChannelInterceptor(AuthorityAuthorizationManager.hasAnyRole())}
*/
@Deprecated(since = "6.0", forRemoval = true)
public class IntegrationSecurityNamespaceHandler extends AbstractIntegrationNamespaceHandler {
public void init() {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2022 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.
@@ -41,7 +41,11 @@ import org.springframework.util.xml.DomUtils;
* @author Jonas Partner
* @author Mark Fisher
* @author Artem Bilan
*
* @deprecated since 6.0 in favor of literally
* {@code new AuthorizationChannelInterceptor(AuthorityAuthorizationManager.hasAnyRole())}
*/
@Deprecated(since = "6.0")
public class SecuredChannelsParser extends AbstractSingleBeanDefinitionParser {
@Override

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2022 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.
@@ -43,7 +43,11 @@ import org.springframework.integration.security.channel.SecuredChannel;
* @author Artem Bilan
*
* @since 4.0
*
* @deprecated since 6.0 in favor of literally
* {@code new AuthorizationChannelInterceptor(AuthorityAuthorizationManager.hasAnyRole())}
*/
@Deprecated(since = "6.0", forRemoval = true)
public class SecurityIntegrationConfigurationInitializer implements IntegrationConfigurationInitializer {
private static final String CHANNEL_SECURITY_INTERCEPTOR_BPP_BEAN_NAME =

View File

@@ -1,43 +0,0 @@
/*
* Copyright 2002-2019 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.integration.security;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
/**
* @author Oleg Zhurakousky
* @author Mark Fisher
* @since 2.0
*/
public class MockAuthenticationManager implements AuthenticationManager {
private final boolean grantAccess;
public MockAuthenticationManager(boolean grantAccess) {
this.grantAccess = grantAccess;
}
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (this.grantAccess) {
authentication.setAuthenticated(true);
}
return authentication;
}
}

View File

@@ -1,22 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/integration"
xmlns:si-security="http://www.springframework.org/schema/integration/security"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:task="http://www.springframework.org/schema/task"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/integration
https://www.springframework.org/schema/integration/spring-integration.xsd
http://www.springframework.org/schema/integration/security
https://www.springframework.org/schema/integration/security/spring-integration-security.xsd
http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/task https://www.springframework.org/schema/task/spring-task.xsd">
<beans:import resource="classpath:org/springframework/integration/security/config/commonSecurityConfiguration.xml"/>
<security:user-service id="userDetailsService">
<security:user name="jimi" password="jimispassword" authorities="ROLE_USER, ROLE_ADMIN"/>
<security:user name="bob" password="bobspassword" authorities="ROLE_USER"/>
</security:user-service>
<si-security:secured-channels>
<si-security:access-policy pattern="securedChannel.*" send-access="ROLE_ADMIN, ROLE_PRESIDENT"/>
</si-security:secured-channels>
<channel-interceptor pattern="securedChannel*">
<beans:bean
class="org.springframework.security.messaging.access.intercept.AuthorizationChannelInterceptor">
<beans:constructor-arg>
<beans:bean class="org.springframework.security.authorization.AuthorityAuthorizationManager"
factory-method="hasAnyRole">
<beans:constructor-arg>
<beans:array>
<beans:value>ADMIN</beans:value>
<beans:value>PRESIDENT</beans:value>
</beans:array>
</beans:constructor-arg>
</beans:bean>
</beans:constructor-arg>
</beans:bean>
</channel-interceptor>
<beans:bean id="testHandler" class="org.springframework.integration.security.TestHandler"/>

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2022 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.
@@ -17,10 +17,10 @@
package org.springframework.integration.security.channel;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
@@ -28,25 +28,22 @@ import org.springframework.integration.security.SecurityTestUtils;
import org.springframework.integration.security.TestHandler;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandlingException;
import org.springframework.messaging.MessageDeliveryException;
import org.springframework.messaging.PollableChannel;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
/**
* @author Mark Fisher
* @author Oleg Zhurakousky
* @author Artem Bilan
*/
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@SpringJUnitConfig
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
public class ChannelAdapterSecurityIntegrationTests {
@@ -78,59 +75,67 @@ public class ChannelAdapterSecurityIntegrationTests {
TestHandler testConsumer;
@After
@AfterEach
public void tearDown() {
SecurityContextHolder.clearContext();
}
@Test(expected = AccessDeniedException.class)
@Test
public void testSecuredWithNotEnoughPermission() {
login("bob", "bobspassword", "ROLE_ADMINA");
securedChannelAdapter.send(new GenericMessage<String>("test"));
assertThatExceptionOfType(MessageDeliveryException.class)
.isThrownBy(() -> this.securedChannelAdapter.send(new GenericMessage<>("test")))
.withRootCauseExactlyInstanceOf(AccessDeniedException.class);
}
@Test
public void testSecuredWithPermission() {
login("bob", "bobspassword", "ROLE_ADMIN", "ROLE_PRESIDENT");
securedChannelAdapter.send(new GenericMessage<String>("test"));
securedChannelAdapter2.send(new GenericMessage<String>("test"));
securedChannelAdapter.send(new GenericMessage<>("test"));
securedChannelAdapter2.send(new GenericMessage<>("test"));
assertThat(testConsumer.sentMessages.size()).as("Wrong size of message list in target").isEqualTo(2);
}
@Test
public void testSecurityContextPropagation() {
login("bob", "bobspassword", "ROLE_ADMIN", "ROLE_PRESIDENT");
this.queueChannel.send(new GenericMessage<String>("test"));
this.queueChannel.send(new GenericMessage<>("test"));
Message<?> receive = this.securedChannelQueue.receive(10000);
assertThat(receive).isNotNull();
SecurityContextHolder.clearContext();
this.queueChannel.send(new GenericMessage<String>("test"));
this.queueChannel.send(new GenericMessage<>("test"));
Message<?> errorMessage = this.errorChannel.receive(10000);
assertThat(errorMessage).isNotNull();
Object payload = errorMessage.getPayload();
assertThat(payload).isInstanceOf(MessageHandlingException.class);
assertThat(((MessageHandlingException) payload).getCause())
assertThat(payload).isInstanceOf(MessageDeliveryException.class);
assertThat(((MessageDeliveryException) payload).getCause())
.isInstanceOf(AuthenticationCredentialsNotFoundException.class);
}
@Test(expected = AccessDeniedException.class)
@Test
public void testSecuredWithoutPermission() {
login("bob", "bobspassword", "ROLE_USER");
securedChannelAdapter.send(new GenericMessage<String>("test"));
assertThatExceptionOfType(MessageDeliveryException.class)
.isThrownBy(() -> this.securedChannelAdapter.send(new GenericMessage<>("test")))
.withRootCauseExactlyInstanceOf(AccessDeniedException.class);
}
@Test(expected = AccessDeniedException.class)
@Test
public void testSecured2WithoutPermission() {
login("bob", "bobspassword", "ROLE_USER");
securedChannelAdapter2.send(new GenericMessage<String>("test"));
assertThatExceptionOfType(MessageDeliveryException.class)
.isThrownBy(() -> this.securedChannelAdapter2.send(new GenericMessage<>("test")))
.withRootCauseExactlyInstanceOf(AccessDeniedException.class);
}
@Test(expected = AuthenticationException.class)
@Test
public void testSecuredWithoutAuthenticating() {
securedChannelAdapter.send(new GenericMessage<String>("test"));
assertThatExceptionOfType(MessageDeliveryException.class)
.isThrownBy(() -> this.securedChannelAdapter.send(new GenericMessage<>("test")))
.withRootCauseExactlyInstanceOf(AuthenticationCredentialsNotFoundException.class);
}
@Test

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2022 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,80 +16,67 @@
package org.springframework.integration.security.channel;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import org.junit.After;
import org.junit.Test;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.security.MockAuthenticationManager;
import org.springframework.integration.security.SecurityTestUtils;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageDeliveryException;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.vote.AffirmativeBased;
import org.springframework.security.access.vote.RoleVoter;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authorization.AuthorityAuthorizationManager;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.messaging.access.intercept.AuthorizationChannelInterceptor;
/**
* @author Mark Fisher
* @author Oleg Zhurakousky
* @author Artem Bilan
*/
public class ChannelSecurityInterceptorTests {
@After
@AfterEach
public void clearSecurityContext() {
SecurityContextHolder.clearContext();
}
@Test(expected = AuthenticationException.class)
public void securedSendWithoutAuthentication() throws Exception {
MessageChannel channel = getSecuredChannel("ROLE_ADMIN");
channel.send(new GenericMessage<String>("test"));
}
@Test(expected = AccessDeniedException.class)
public void securedSendWithoutRole() throws Exception {
MessageChannel channel = getSecuredChannel("ROLE_ADMIN");
SecurityContext context = SecurityTestUtils.createContext("test", "pwd", "ROLE_USER");
SecurityContextHolder.setContext(context);
channel.send(new GenericMessage<String>("test"));
@Test
public void securedSendWithoutAuthentication() {
MessageChannel channel = getSecuredChannel();
assertThatExceptionOfType(MessageDeliveryException.class)
.isThrownBy(() -> channel.send(new GenericMessage<>("test")))
.withRootCauseExactlyInstanceOf(AuthenticationCredentialsNotFoundException.class);
}
@Test
public void securedSendWithRole() throws Exception {
MessageChannel channel = getSecuredChannel("ROLE_ADMIN");
public void securedSendWithoutRole() {
MessageChannel channel = getSecuredChannel();
SecurityContext context = SecurityTestUtils.createContext("test", "pwd", "ROLE_USER");
SecurityContextHolder.setContext(context);
assertThatExceptionOfType(MessageDeliveryException.class)
.isThrownBy(() -> channel.send(new GenericMessage<>("test")))
.withRootCauseExactlyInstanceOf(AccessDeniedException.class);
}
@Test
public void securedSendWithRole() {
MessageChannel channel = getSecuredChannel();
SecurityContext context = SecurityTestUtils.createContext("test", "pwd", "ROLE_ADMIN");
SecurityContextHolder.setContext(context);
channel.send(new GenericMessage<String>("test"));
channel.send(new GenericMessage<>("test"));
}
private static MessageChannel getSecuredChannel(String role) throws Exception {
private static MessageChannel getSecuredChannel() {
QueueChannel channel = new QueueChannel();
channel.setBeanName("securedChannel");
ProxyFactory proxyFactory = new ProxyFactory(channel);
proxyFactory.addAdvice(createInterceptor(role));
return (MessageChannel) proxyFactory.getProxy();
}
@SuppressWarnings("rawtypes")
private static ChannelSecurityInterceptor createInterceptor(String role) throws Exception {
ChannelSecurityMetadataSource securityMetadataSource = new ChannelSecurityMetadataSource();
securityMetadataSource.addPatternMapping(Pattern.compile("secured.*"), new DefaultChannelAccessPolicy(role, null));
ChannelSecurityInterceptor interceptor = new ChannelSecurityInterceptor(securityMetadataSource);
AffirmativeBased accessDecisionManager = AffirmativeBased.class.getConstructor(List.class)
.newInstance(Collections.singletonList(new RoleVoter()));
accessDecisionManager.afterPropertiesSet();
interceptor.setAccessDecisionManager(accessDecisionManager);
interceptor.setAuthenticationManager(new MockAuthenticationManager(true));
interceptor.afterPropertiesSet();
return interceptor;
channel.addInterceptor(new AuthorizationChannelInterceptor(AuthorityAuthorizationManager.hasRole("ADMIN")));
return channel;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2022 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.
@@ -17,20 +17,19 @@
package org.springframework.integration.security.config;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
@@ -50,13 +49,11 @@ import org.springframework.integration.config.GlobalChannelInterceptor;
import org.springframework.integration.handler.BridgeHandler;
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.integration.security.channel.SecurityContextPropagationChannelInterceptor;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageDeliveryException;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessageHandlingException;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.PollableChannel;
import org.springframework.messaging.SubscribableChannel;
@@ -64,27 +61,27 @@ import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.authorization.AuthorityAuthorizationManager;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.messaging.access.intercept.AuthorizationChannelInterceptor;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.task.DelegatingSecurityContextAsyncTaskExecutor;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
/**
* @author Artem Bilan
*
* @since 4.0
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@SpringJUnitConfig
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
public class ChannelSecurityInterceptorSecuredChannelAnnotationTests {
public class ChannelSecurityAnnotationTests {
@Autowired
MessageChannel securedChannel;
@@ -125,96 +122,104 @@ public class ChannelSecurityInterceptorSecuredChannelAnnotationTests {
@Autowired
TestGateway testGateway;
@After
@AfterEach
public void tearDown() {
SecurityContextHolder.clearContext();
}
@Test(expected = AccessDeniedException.class)
@Test
public void testSecuredWithNotEnoughPermission() {
login("bob", "bobspassword", "ROLE_ADMINA");
securedChannel.send(new GenericMessage<String>("test"));
assertThatExceptionOfType(MessageDeliveryException.class)
.isThrownBy(() -> this.securedChannel.send(new GenericMessage<>("test")))
.withRootCauseExactlyInstanceOf(AccessDeniedException.class);
}
@Test
public void testSecuredWithPermission() {
login("bob", "bobspassword", "ROLE_ADMIN", "ROLE_PRESIDENT");
securedChannel.send(new GenericMessage<String>("test"));
securedChannel2.send(new GenericMessage<String>("test"));
securedChannel.send(new GenericMessage<>("test"));
securedChannel2.send(new GenericMessage<>("test"));
assertThat(testConsumer.sentMessages.size()).as("Wrong size of message list in target").isEqualTo(2);
}
@Test(expected = AccessDeniedException.class)
@Test
public void testSecuredWithoutPermision() {
login("bob", "bobspassword", "ROLE_USER");
securedChannel.send(new GenericMessage<String>("test"));
assertThatExceptionOfType(MessageDeliveryException.class)
.isThrownBy(() -> this.securedChannel.send(new GenericMessage<>("test")))
.withRootCauseExactlyInstanceOf(AccessDeniedException.class);
}
@Test(expected = AccessDeniedException.class)
@Test
public void testSecured2WithoutPermision() {
login("bob", "bobspassword", "ROLE_USER");
securedChannel2.send(new GenericMessage<String>("test"));
assertThatExceptionOfType(MessageDeliveryException.class)
.isThrownBy(() -> this.securedChannel2.send(new GenericMessage<>("test")))
.withRootCauseExactlyInstanceOf(AccessDeniedException.class);
}
@Test(expected = AuthenticationException.class)
@Test
public void testSecuredWithoutAuthenticating() {
securedChannel.send(new GenericMessage<String>("test"));
assertThatExceptionOfType(MessageDeliveryException.class)
.isThrownBy(() -> this.securedChannel2.send(new GenericMessage<>("test")))
.withRootCauseExactlyInstanceOf(AuthenticationCredentialsNotFoundException.class);
}
@Test
public void testUnsecuredAsAdmin() {
login("bob", "bobspassword", "ROLE_ADMIN");
unsecuredChannel.send(new GenericMessage<String>("test"));
unsecuredChannel.send(new GenericMessage<>("test"));
assertThat(testConsumer.sentMessages.size()).as("Wrong size of message list in target").isEqualTo(1);
}
@Test
public void testUnsecuredAsUser() {
login("bob", "bobspassword", "ROLE_USER");
unsecuredChannel.send(new GenericMessage<String>("test"));
unsecuredChannel.send(new GenericMessage<>("test"));
assertThat(testConsumer.sentMessages.size()).as("Wrong size of message list in target").isEqualTo(1);
}
@Test
public void testUnsecuredWithoutAuthenticating() {
unsecuredChannel.send(new GenericMessage<String>("test"));
unsecuredChannel.send(new GenericMessage<>("test"));
assertThat(testConsumer.sentMessages.size()).as("Wrong size of message list in target").isEqualTo(1);
}
@Test
public void testSecurityContextPropagationQueueChannel() {
login("bob", "bobspassword", "ROLE_ADMIN", "ROLE_PRESIDENT");
this.queueChannel.send(new GenericMessage<String>("test"));
this.queueChannel.send(new GenericMessage<>("test"));
Message<?> receive = this.securedChannelQueue.receive(10000);
assertThat(receive).isNotNull();
SecurityContextHolder.clearContext();
this.queueChannel.send(new GenericMessage<String>("test"));
this.queueChannel.send(new GenericMessage<>("test"));
Message<?> errorMessage = this.errorChannel.receive(10000);
assertThat(errorMessage).isNotNull();
Object payload = errorMessage.getPayload();
assertThat(payload).isInstanceOf(MessageHandlingException.class);
assertThat(((MessageHandlingException) payload).getCause())
assertThat(payload).isInstanceOf(MessageDeliveryException.class);
assertThat(((MessageDeliveryException) payload).getCause())
.isInstanceOf(AuthenticationCredentialsNotFoundException.class);
}
@Test
public void testSecurityContextPropagationExecutorChannel() {
login("bob", "bobspassword", "ROLE_ADMIN", "ROLE_PRESIDENT");
this.executorChannel.send(new GenericMessage<String>("test"));
this.executorChannel.send(new GenericMessage<>("test"));
Message<?> receive = this.securedChannelQueue.receive(10000);
assertThat(receive).isNotNull();
SecurityContextHolder.clearContext();
this.executorChannel.send(new GenericMessage<String>("test"));
this.executorChannel.send(new GenericMessage<>("test"));
Message<?> errorMessage = this.errorChannel.receive(10000);
assertThat(errorMessage).isNotNull();
Object payload = errorMessage.getPayload();
assertThat(payload).isInstanceOf(MessageHandlingException.class);
assertThat(((MessageHandlingException) payload).getCause())
assertThat(payload).isInstanceOf(MessageDeliveryException.class);
assertThat(((MessageDeliveryException) payload).getCause())
.isInstanceOf(AuthenticationCredentialsNotFoundException.class);
}
@@ -222,7 +227,7 @@ public class ChannelSecurityInterceptorSecuredChannelAnnotationTests {
public void testSecurityContextPropagationPublishSubscribeChannel() {
login("bob", "bobspassword", "ROLE_ADMIN", "ROLE_PRESIDENT");
this.publishSubscribeChannel.send(new GenericMessage<String>("test"));
this.publishSubscribeChannel.send(new GenericMessage<>("test"));
Message<?> receive = this.securedChannelQueue.receive(10000);
assertThat(receive).isNotNull();
@@ -236,7 +241,7 @@ public class ChannelSecurityInterceptorSecuredChannelAnnotationTests {
this.publishSubscribeChannel.setApplySequence(true);
this.publishSubscribeChannel.send(new GenericMessage<String>("test"));
this.publishSubscribeChannel.send(new GenericMessage<>("test"));
receive = this.securedChannelQueue.receive(10000);
assertThat(receive).isNotNull();
@@ -252,12 +257,12 @@ public class ChannelSecurityInterceptorSecuredChannelAnnotationTests {
SecurityContextHolder.clearContext();
this.publishSubscribeChannel.send(new GenericMessage<String>("test"));
this.publishSubscribeChannel.send(new GenericMessage<>("test"));
Message<?> errorMessage = this.errorChannel.receive(10000);
assertThat(errorMessage).isNotNull();
Object payload = errorMessage.getPayload();
assertThat(payload).isInstanceOf(MessageHandlingException.class);
assertThat(((MessageHandlingException) payload).getCause())
assertThat(payload).isInstanceOf(MessageDeliveryException.class);
assertThat(((MessageDeliveryException) payload).getCause())
.isInstanceOf(AuthenticationCredentialsNotFoundException.class);
}
@@ -285,22 +290,38 @@ public class ChannelSecurityInterceptorSecuredChannelAnnotationTests {
@Configuration
@EnableIntegration
@IntegrationComponentScan
@ImportResource("classpath:org/springframework/integration/security/config/commonSecurityConfiguration.xml")
public static class ContextConfiguration {
@Bean
UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager(
User.withUsername("jimi")
.password("jimispassword")
.authorities("ROLE_USER", "ROLE_ADMIN")
.build(),
User.withUsername("bob")
.password("bobspassword")
.authorities("ROLE_USER")
.build());
}
@Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
@Bean
@SecuredChannel(interceptor = "channelSecurityInterceptor", sendAccess = {"ROLE_ADMIN", "ROLE_PRESIDENT"})
@GlobalChannelInterceptor(patterns = "secured*")
AuthorizationChannelInterceptor authorizationChannelInterceptor() {
return new AuthorizationChannelInterceptor(AuthorityAuthorizationManager.hasAnyRole("ADMIN", "PRESIDENT"));
}
@Bean
public SubscribableChannel securedChannel() {
return new DirectChannel();
}
@Bean
@SecuredChannel(interceptor = "channelSecurityInterceptor", sendAccess = {"ROLE_ADMIN", "ROLE_PRESIDENT"})
public SubscribableChannel securedChannel2() {
return new DirectChannel();
}
@@ -326,7 +347,6 @@ public class ChannelSecurityInterceptorSecuredChannelAnnotationTests {
}
@Bean
@SecuredChannel(interceptor = "channelSecurityInterceptor", sendAccess = {"ROLE_ADMIN", "ROLE_PRESIDENT"})
public PollableChannel securedChannelQueue() {
return new QueueChannel();
}
@@ -353,7 +373,6 @@ public class ChannelSecurityInterceptorSecuredChannelAnnotationTests {
}
@Bean
@SecuredChannel(interceptor = "channelSecurityInterceptor", sendAccess = {"ROLE_ADMIN", "ROLE_PRESIDENT"})
public PollableChannel securedChannelQueue2() {
return new QueueChannel();
}
@@ -386,15 +405,6 @@ public class ChannelSecurityInterceptorSecuredChannelAnnotationTests {
return testHandler;
}
@Bean
public ChannelSecurityInterceptor channelSecurityInterceptor(AuthenticationManager authenticationManager,
AccessDecisionManager accessDecisionManager) {
ChannelSecurityInterceptor channelSecurityInterceptor = new ChannelSecurityInterceptor();
channelSecurityInterceptor.setAuthenticationManager(authenticationManager);
channelSecurityInterceptor.setAccessDecisionManager(accessDecisionManager);
return channelSecurityInterceptor;
}
@Bean
public AsyncTaskExecutor securityContextExecutor() {
return new DelegatingSecurityContextAsyncTaskExecutor(new SimpleAsyncTaskExecutor());

View File

@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:security="http://www.springframework.org/schema/integration/security"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/integration/security
https://www.springframework.org/schema/integration/security/spring-integration-security.xsd">
<import resource="classpath:org/springframework/integration/security/config/commonSecurityConfiguration.xml"/>
<security:secured-channels>
<security:access-policy pattern="test" send-access="ROLE_ADMIN"/>
</security:secured-channels>
</beans>

View File

@@ -1,81 +0,0 @@
/*
* Copyright 2002-2019 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.integration.security.config;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.integration.channel.MessagePublishingErrorHandler;
import org.springframework.integration.channel.NullChannel;
import org.springframework.integration.channel.PublishSubscribeChannel;
import org.springframework.integration.context.IntegrationContextUtils;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.messaging.MessageChannel;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.util.ErrorHandler;
/**
* @author Mark Fisher
* @author Artem Bilan
* @author Gary Russell
* @since 1.0.3
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class DefaultConfigurationTests {
@Autowired
private ApplicationContext context;
@Test
public void verifyErrorChannel() {
Object errorChannel = context.getBean("errorChannel");
assertThat(errorChannel).isNotNull();
assertThat(errorChannel.getClass()).isEqualTo(PublishSubscribeChannel.class);
}
@Test
public void verifyNullChannel() {
Object nullChannel = context.getBean("nullChannel");
assertThat(nullChannel).isNotNull();
assertThat(nullChannel.getClass()).isEqualTo(NullChannel.class);
}
@Test
public void verifyTaskScheduler() {
Object taskScheduler = context.getBean(IntegrationContextUtils.TASK_SCHEDULER_BEAN_NAME);
assertThat(taskScheduler.getClass()).isEqualTo(ThreadPoolTaskScheduler.class);
ErrorHandler errorHandler = TestUtils.getPropertyValue(taskScheduler, "errorHandler", ErrorHandler.class);
assertThat(errorHandler.getClass()).isEqualTo(MessagePublishingErrorHandler.class);
MessageChannel defaultErrorChannel = TestUtils.getPropertyValue(errorHandler,
"messagingTemplate.defaultDestination", MessageChannel.class);
assertThat(defaultErrorChannel).isNull();
errorHandler.handleError(new Throwable());
defaultErrorChannel = TestUtils.getPropertyValue(errorHandler, "messagingTemplate.defaultDestination",
MessageChannel.class);
assertThat(defaultErrorChannel).isNotNull();
assertThat(defaultErrorChannel).isEqualTo(context.getBean(IntegrationContextUtils.ERROR_CHANNEL_BEAN_NAME));
}
}

View File

@@ -1,29 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/integration"
xmlns:si-security="http://www.springframework.org/schema/integration/security"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:security="http://www.springframework.org/schema/security"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security
https://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/integration
https://www.springframework.org/schema/integration/spring-integration.xsd
http://www.springframework.org/schema/integration/security
https://www.springframework.org/schema/integration/security/spring-integration-security.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<beans:import resource="classpath:org/springframework/integration/security/config/commonSecurityConfiguration.xml"/>
<si-security:secured-channels>
<si-security:access-policy pattern="adminRequiredForSend" send-access="ROLE_ADMIN"/>
<si-security:access-policy pattern="adminOrUserRequiredForSend" send-access="ROLE_ADMIN, ROLE_USER"/>
<si-security:access-policy pattern="adminRequiredForReceive" receive-access="ROLE_ADMIN"/>
<si-security:access-policy pattern="adminOrUserRequiredForReceive" receive-access="ROLE_ADMIN, ROLE_USER"/>
<si-security:access-policy pattern="adminRequiredForSendAndReceive" send-access="ROLE_ADMIN" receive-access="ROLE_ADMIN"/>
</si-security:secured-channels>
</beans:beans>

View File

@@ -1,216 +0,0 @@
/*
* Copyright 2002-2019 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.integration.security.config;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import org.junit.Before;
import org.junit.Test;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.integration.channel.AbstractPollableChannel;
import org.springframework.integration.core.MessageSelector;
import org.springframework.integration.security.channel.ChannelAccessPolicy;
import org.springframework.integration.security.channel.ChannelSecurityInterceptor;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests;
/**
* @author Jonas Partner
* @author Mark Fisher
* @author Oleg Zhurakousky
*/
@ContextConfiguration
public class SecuredChannelsParserTests extends AbstractJUnit4SpringContextTests {
TestMessageChannel messageChannel;
@Before
public void setUp() {
messageChannel = new TestMessageChannel();
}
@Test
public void testAdminRequiredForSend() {
String beanName = "adminRequiredForSend";
messageChannel.setBeanName(beanName);
MessageChannel proxy = (MessageChannel) applicationContext.getAutowireCapableBeanFactory()
.applyBeanPostProcessorsAfterInitialization(messageChannel, beanName);
assertThat(AopUtils.isAopProxy(proxy)).as("Channel was not proxied").isTrue();
Advisor[] advisors = ((Advised) proxy).getAdvisors();
assertThat(advisors.length).as("Wrong number of interceptors").isEqualTo(1);
ChannelSecurityInterceptor interceptor = (ChannelSecurityInterceptor) advisors[0].getAdvice();
ChannelAccessPolicy policy = this.retrievePolicyForPatternString(beanName, interceptor);
assertThat(policy).as("Pattern '" + beanName + "' is not included in mappings").isNotNull();
Collection<ConfigAttribute> sendDefinition = policy.getConfigAttributesForSend();
Collection<ConfigAttribute> receiveDefinition = policy.getConfigAttributesForReceive();
assertThat(this.getRolesFromDefintion(sendDefinition).contains("ROLE_ADMIN"))
.as("ROLE_ADMIN not found as send attribute").isTrue();
assertThat(receiveDefinition.size() == 0).as("Policy applies to receive").isTrue();
}
@Test
public void testAdminOrUserRequiredForSend() {
String beanName = "adminOrUserRequiredForSend";
messageChannel.setBeanName(beanName);
MessageChannel proxy = (MessageChannel) applicationContext.getAutowireCapableBeanFactory()
.applyBeanPostProcessorsAfterInitialization(messageChannel, beanName);
assertThat(AopUtils.isAopProxy(proxy)).as("Channel was not proxied").isTrue();
Advisor[] advisors = ((Advised) proxy).getAdvisors();
assertThat(advisors.length).as("Wrong number of interceptors").isEqualTo(1);
ChannelSecurityInterceptor interceptor = (ChannelSecurityInterceptor) advisors[0].getAdvice();
ChannelAccessPolicy policy = this.retrievePolicyForPatternString(beanName, interceptor);
assertThat(policy).as("Pattern '" + beanName + "' is not included in mappings").isNotNull();
Collection<ConfigAttribute> sendDefinition = policy.getConfigAttributesForSend();
Collection<ConfigAttribute> receiveDefinition = policy.getConfigAttributesForReceive();
Collection<String> sendRoles = this.getRolesFromDefintion(sendDefinition);
assertThat(sendRoles.contains("ROLE_ADMIN")).as("ROLE_ADMIN not found as send attribute").isTrue();
assertThat(sendRoles.contains("ROLE_USER")).as("ROLE_USER not found as send attribute").isTrue();
assertThat(receiveDefinition.size() == 0).as("Policy applies to receive").isTrue();
}
@Test
public void testAdminRequiredForReceive() {
String beanName = "adminRequiredForReceive";
messageChannel.setBeanName(beanName);
MessageChannel proxy = (MessageChannel) applicationContext.getAutowireCapableBeanFactory()
.applyBeanPostProcessorsAfterInitialization(messageChannel, beanName);
assertThat(AopUtils.isAopProxy(proxy)).as("Channel was not proxied").isTrue();
Advisor[] advisors = ((Advised) proxy).getAdvisors();
assertThat(advisors.length).as("Wrong number of interceptors").isEqualTo(1);
ChannelSecurityInterceptor interceptor = (ChannelSecurityInterceptor) advisors[0].getAdvice();
ChannelAccessPolicy policy = this.retrievePolicyForPatternString(beanName, interceptor);
assertThat(policy).as("Pattern '" + beanName + "' is not included in mappings").isNotNull();
Collection<ConfigAttribute> sendDefinition = policy.getConfigAttributesForSend();
Collection<ConfigAttribute> receiveDefinition = policy.getConfigAttributesForReceive();
Collection<String> receiveRoles = this.getRolesFromDefintion(receiveDefinition);
assertThat(receiveRoles.contains("ROLE_ADMIN")).as("ROLE_ADMIN not found as receive attribute").isTrue();
assertThat(sendDefinition.size() == 0).as("Policy applies to receive").isTrue();
}
@Test
public void testAdminOrUserRequiredForReceive() {
String beanName = "adminOrUserRequiredForReceive";
messageChannel.setBeanName(beanName);
MessageChannel proxy = (MessageChannel) applicationContext.getAutowireCapableBeanFactory()
.applyBeanPostProcessorsAfterInitialization(messageChannel, beanName);
assertThat(AopUtils.isAopProxy(proxy)).as("Channel was not proxied").isTrue();
Advisor[] advisors = ((Advised) proxy).getAdvisors();
assertThat(advisors.length).as("Wrong number of interceptors").isEqualTo(1);
ChannelSecurityInterceptor interceptor = (ChannelSecurityInterceptor) advisors[0].getAdvice();
ChannelAccessPolicy policy = this.retrievePolicyForPatternString(beanName, interceptor);
assertThat(policy).as("Pattern '" + beanName + "' is not included in mappings").isNotNull();
Collection<ConfigAttribute> sendDefinition = policy.getConfigAttributesForSend();
Collection<ConfigAttribute> receiveDefinition = policy.getConfigAttributesForReceive();
Collection<String> receiveRoles = this.getRolesFromDefintion(receiveDefinition);
assertThat(receiveRoles.contains("ROLE_ADMIN")).as("ROLE_ADMIN not found as receive attribute").isTrue();
assertThat(receiveRoles.contains("ROLE_USER")).as("ROLE_USER not found as receive attribute").isTrue();
assertThat(sendDefinition.size() == 0).as("Policy applies to receive").isTrue();
}
@Test
public void testAdminRequiredForSendAndReceive() {
String beanName = "adminRequiredForSendAndReceive";
messageChannel.setBeanName(beanName);
MessageChannel proxy = (MessageChannel) applicationContext.getAutowireCapableBeanFactory()
.applyBeanPostProcessorsAfterInitialization(messageChannel, beanName);
assertThat(AopUtils.isAopProxy(proxy)).as("Channel was not proxied").isTrue();
Advisor[] advisors = ((Advised) proxy).getAdvisors();
assertThat(advisors.length).as("Wrong number of interceptors").isEqualTo(1);
ChannelSecurityInterceptor interceptor = (ChannelSecurityInterceptor) advisors[0].getAdvice();
ChannelAccessPolicy policy = this.retrievePolicyForPatternString(beanName, interceptor);
assertThat(policy).as("Pattern '" + beanName + "' is not included in mappings").isNotNull();
Collection<ConfigAttribute> sendDefinition = policy.getConfigAttributesForSend();
Collection<ConfigAttribute> receiveDefinition = policy.getConfigAttributesForReceive();
assertThat(sendDefinition).as("Pattern does not apply to 'send'").isNotNull();
assertThat(receiveDefinition).as("Pattern does not apply to 'receive'").isNotNull();
Collection<String> sendRoles = this.getRolesFromDefintion(sendDefinition);
Collection<String> receiveRoles = this.getRolesFromDefintion(receiveDefinition);
assertThat(sendRoles.contains("ROLE_ADMIN")).as("ROLE_ADMIN not found in send attributes").isTrue();
assertThat(receiveRoles.contains("ROLE_ADMIN")).as("ROLE_ADMIN not found in receive attributes").isTrue();
}
@SuppressWarnings("unchecked")
private ChannelAccessPolicy retrievePolicyForPatternString(String patternString, ChannelSecurityInterceptor interceptor) {
DirectFieldAccessor accessor = new DirectFieldAccessor(interceptor.obtainSecurityMetadataSource());
Map<Pattern, ChannelAccessPolicy> policies = (Map<Pattern, ChannelAccessPolicy>) accessor.getPropertyValue("patternMappings");
for (Map.Entry<Pattern, ChannelAccessPolicy> entry : policies.entrySet()) {
if (entry.getKey().pattern().equals(patternString)) {
return entry.getValue();
}
}
return null;
}
private Collection<String> getRolesFromDefintion(Collection<ConfigAttribute> definition) {
Set<String> roles = new HashSet<String>();
//Collection configAttributes = SecurityConfig.createListFromCommaDelimitedString(definition);
for (ConfigAttribute nextConfigAttribute : definition) {
ConfigAttribute attribute = nextConfigAttribute;
roles.add(attribute.getAttribute());
}
return roles;
}
static class TestMessageChannel extends AbstractPollableChannel {
List<ChannelInterceptor> interceptors = new ArrayList<ChannelInterceptor>();
@Override
protected Message<?> doReceive(long timeout) {
return null;
}
@Override
protected boolean doSend(Message<?> message, long timeout) {
return false;
}
public List<Message<?>> clear() {
return null;
}
public List<Message<?>> purge(MessageSelector selector) {
return null;
}
@Override
public void addInterceptor(ChannelInterceptor interceptor) {
interceptors.add(interceptor);
}
}
}

View File

@@ -1,31 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/integration"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:security="http://www.springframework.org/schema/security"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<context:annotation-config/>
<beans:bean id="accessDecisionManager" class="org.springframework.security.access.vote.AffirmativeBased">
<beans:constructor-arg>
<beans:bean class="org.springframework.security.access.vote.RoleVoter"/>
</beans:constructor-arg>
<beans:property name="allowIfAllAbstainDecisions" value="true"/>
</beans:bean>
<security:authentication-manager alias="authenticationManager">
<security:authentication-provider user-service-ref="userDetailsService"/>
</security:authentication-manager>
<security:user-service id="userDetailsService">
<security:user name="jimi" password="jimispassword" authorities="ROLE_USER, ROLE_ADMIN"/>
<security:user name="bob" password="bobspassword" authorities="ROLE_USER"/>
</security:user-service>
</beans:beans>

View File

@@ -28,7 +28,6 @@ import java.util.Collections;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.reactivestreams.Publisher;
@@ -59,9 +58,6 @@ import org.springframework.integration.webflux.outbound.WebFluxRequestExecutingM
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.PollableChannel;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.vote.AffirmativeBased;
import org.springframework.security.access.vote.RoleVoter;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
@@ -294,7 +290,6 @@ public class WebFluxDslTests {
private Validator validator;
@Test
@Disabled("Fails after some recent SF change")
public void testValidation() {
IntegrationFlow flow =
IntegrationFlow.from(
@@ -482,11 +477,6 @@ public class WebFluxDslTests {
.get();
}
@Bean
public AccessDecisionManager accessDecisionManager() {
return new AffirmativeBased(Collections.singletonList(new RoleVoter()));
}
}
public static class TestModel {

View File

@@ -7,6 +7,10 @@ Messaging independence and loose coupling let target systems communicate with ea
We can either trust all those messages or secure our service against "`infecting`" messages.
Spring Integration, together with https://projects.spring.io/spring-security/[Spring Security], provides a simple and comprehensive way to secure message channels, as well as other part of the integration solution.
Starting with version 6.0, the `ChannelSecurityInterceptor` as well as its configuration via `@SecuredChannel` annotation and XML `<secured-channels>` have been deprecation in favor of using `AuthorizationChannelInterceptor` from the `spring-security-messaging` module.
The respective `AuthorizationManager` infrastructure fully covers the previously supported role-based authentication, plus it allows the configuration of any other possible authorization strategies.
The only remaining Spring Integration feature is a `SecurityContextPropagationChannelInterceptor` which may be promoted to the mentioned `spring-security-messaging` module in the future as well.
You need to include this dependency into your project:
@@ -30,93 +34,41 @@ compile "org.springframework.integration:spring-integration-security:{project-ve
[[securing-channels]]
=== Securing channels
Spring Integration provides the `ChannelSecurityInterceptor` interceptor, which extends `AbstractSecurityInterceptor` and intercepts send and receive calls on the channel.
Access decisions are then made with reference to a `ChannelSecurityMetadataSource`, which provides the metadata that describes the `send()` and `receive()` access policies for certain channels.
The interceptor requires that a valid `SecurityContext` has been established by authenticating with Spring Security.
See the https://docs.spring.io/spring-security/reference/[Spring Security Reference Guide] for details.
Spring Integration provides Namespace support to allow easy configuration of security constraints.
This support consists of the secured channels tag, which allows definition of one or more channel name patterns in conjunction with a definition of the security configuration for `send()` and `receive()`.
The pattern is a `java.util.regexp.Pattern`.
The following example shows how to configure a bean that includes security and how to set up policies with patterns:
To secure message channels in the integration flow, an `AuthorizationChannelInterceptor` has to be added to those channels, or it can be configured as a global channel interceptor with respective pattern:
====
[source,xml]
[source, java, role="primary"]
.Java
----
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns:int="http://www.springframework.org/schema/integration"
xmlns:int-security="http://www.springframework.org/schema/integration/security"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security
https://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/integration
https://www.springframework.org/schema/integration/spring-integration.xsd
http://www.springframework.org/schema/integration/security
https://www.springframework.org/schema/integration/security/spring-integration-security.xsd">
<int-security:secured-channels>
<int-security:access-policy pattern="admin.*" send-access="ROLE_ADMIN"/>
<int-security:access-policy pattern="user.*" receive-access="ROLE_USER"/>
</int-security:secured-channels>
----
====
By default, the `secured-channels` namespace element expects a bean named `authenticationManager` (which implements `AuthenticationManager`) and a bean named `accessDecisionManager` (which implements `AccessDecisionManager`).
Where this is not the case, references to the appropriate beans can be configured as attributes of the `secured-channels` element, as the following example shows:
====
[source,xml]
----
<int-security:secured-channels access-decision-manager="customAccessDecisionManager"
authentication-manager="customAuthenticationManager">
<int-security:access-policy pattern="admin.*" send-access="ROLE_ADMIN"/>
<int-security:access-policy pattern="user.*" receive-access="ROLE_USER"/>
</int-security:secured-channels>
----
====
Starting with version 4.2, the `@SecuredChannel` annotation is available for Java configuration in `@Configuration` classes.
The following example shows the Java equivalent of the preceding XML examples:
====
[source,java]
----
@Configuration
@EnableIntegration
public class ContextConfiguration {
@Bean
@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;
}
@Bean
@GlobalChannelInterceptor(patterns = "secured*")
AuthorizationChannelInterceptor authorizationChannelInterceptor() {
return new AuthorizationChannelInterceptor(AuthorityAuthorizationManager.hasAnyRole("ADMIN", "PRESIDENT"));
}
----
[source, xml, role="secondary"]
.XML
----
<channel-interceptor pattern="securedChannel*">
<beans:bean class="org.springframework.security.messaging.access.intercept.AuthorizationChannelInterceptor">
<beans:constructor-arg>
<beans:bean class="org.springframework.security.authorization.AuthorityAuthorizationManager"
factory-method="hasAnyRole">
<beans:constructor-arg>
<beans:array>
<beans:value>ADMIN</beans:value>
<beans:value>PRESIDENT</beans:value>
</beans:array>
</beans:constructor-arg>
</beans:bean>
</beans:constructor-arg>
</beans:bean>
</channel-interceptor>
----
====
See <<./channel.adoc#global-channel-configuration-interceptors,Global Channel Interceptor Configuration>> for more information.
[[security-context-propagation]]
=== Security Context Propagation
@@ -176,8 +128,6 @@ public class ContextConfiguration {
}
...
@MessagingGateway(asyncExecutor = "securityContextExecutor")
public interface SecuredGateway {

View File

@@ -160,3 +160,9 @@ See <<./ip.adoc#ip,TCP and UDP Support>> for more information.
The `JmsOutboundGateway` now creates a `TemporaryTopic` instead of `TemporaryQueue` if `replyPubSubDomain` option is set to `true`.
See <<./jms.adoc#jms,JMS Support>> for more information.
=== Security Changes
The `ChannelSecurityInterceptor` and its annotation `@SecuredChannel` and XML `<secured-channels>` configurations have been deprecated in favor of `AuthorizationChannelInterceptor`.
See <<./security.adoc#security,Security Support>> for more information.