INT-4072 Fix applySequence with State Propagation

JIRA: https://jira.spring.io/browse/INT-4072

When `publishSubscribeChannel` is with `applySequence = true`, a `messageToSend` is overridden with `sequenceDetails` using `MessageBuilder`, therefore a new fresh `Message`.
In case of state propagation, e.g. `SecurityContextPropagationChannelInterceptor`, we just lost the state from the `ThreadStatePropagationChannelInterceptor.MessageWithThreadState` because of new `Message<?>`

* Add into `BroadcastingDispatcher` the logic to delegate `pushSequenceDetails` into `MessageWithThreadState` directly do not lose the `state`
* Make `ThreadStatePropagationChannelInterceptor` as `MessageBuilderFactory`-aware and use it to rebuild an `original` `Message<?>` in the `MessageWithThreadState` to populate `SequenceDetails`

**Cherry-pick to 4.2.x**

Provide an explicit order for `publishSubscribeChannel` subscribers

Fixes GH-1847 (https://github.com/spring-projects/spring-integration/issues/1847)

Fix mutation in the `ThreadStatePropagationChannelInterceptor`

Since `BroadcastingDispatcher` invokes `pushSequenceDetails` for each subscribed handler,
make `MessageWithThreadState` as immutable and return a new instance via `cloneWithSequenceDetails()` method with particular `sequenceDetails`.
Previous mutable solution ended up with the issue of concurrent modification.

* Introduce `CloneableMessage` abstraction to let any custom `Message` to return `MessageBuilder` with desired context.
* Introduce `DelegatingMessageBuilder` as an extension of the `MessageBuilder` to let custom `CloneableMessage` to return desired customization.
* Add into `MessageBuilder#fromMessage()` `if` for the `CloneableMessage`
* Add into `MutableMessageBuilder` a `warn` about `CloneableMessage`
* Revert changes in the `BroadcastingDispatcher` in favor of `CloneableMessage` in the `MessageBuilder`
* Redo `ThreadStatePropagationChannelInterceptor#MessageWithThreadState` logic to be based on the `CloneableMessage` and `DelegatingMessageBuilder` extension.

Introduce `MessageDecorator` contract

Remove `CloneableMessage` aspect and everything around
`MessageWithThreadState` is now `MessageDecorator` and `BroadcastingDispatcher` check if incoming `message` is `MessageDecorator` and performs its `decorateMessage` after `builder`
This commit is contained in:
Artem Bilan
2016-07-13 18:36:56 -04:00
committed by Gary Russell
parent bce851576e
commit 999644a530
4 changed files with 146 additions and 9 deletions

View File

@@ -33,13 +33,17 @@ 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.integration.IntegrationMessageHeaderAccessor;
import org.springframework.integration.annotation.BridgeTo;
import org.springframework.integration.annotation.Poller;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.channel.ExecutorChannel;
import org.springframework.integration.channel.PublishSubscribeChannel;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.config.EnableIntegration;
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;
@@ -47,6 +51,7 @@ 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.MessageHandler;
import org.springframework.messaging.MessageHandlingException;
import org.springframework.messaging.PollableChannel;
import org.springframework.messaging.SubscribableChannel;
@@ -95,6 +100,14 @@ public class ChannelSecurityInterceptorSecuredChannelAnnotationTests {
@Qualifier("executorChannel")
MessageChannel executorChannel;
@Autowired
@Qualifier("publishSubscribeChannel")
PublishSubscribeChannel publishSubscribeChannel;
@Autowired
@Qualifier("securedChannelQueue2")
PollableChannel securedChannelQueue2;
@Autowired
@Qualifier("errorChannel")
PollableChannel errorChannel;
@@ -170,7 +183,7 @@ public class ChannelSecurityInterceptorSecuredChannelAnnotationTests {
SecurityContextHolder.clearContext();
this.queueChannel.send(new GenericMessage<String>("test"));
Message<?> errorMessage = this.errorChannel.receive(1000);
Message<?> errorMessage = this.errorChannel.receive(10000);
assertNotNull(errorMessage);
Object payload = errorMessage.getPayload();
assertThat(payload, instanceOf(MessageHandlingException.class));
@@ -187,8 +200,8 @@ public class ChannelSecurityInterceptorSecuredChannelAnnotationTests {
SecurityContextHolder.clearContext();
this.queueChannel.send(new GenericMessage<String>("test"));
Message<?> errorMessage = this.errorChannel.receive(1000);
this.executorChannel.send(new GenericMessage<String>("test"));
Message<?> errorMessage = this.errorChannel.receive(10000);
assertNotNull(errorMessage);
Object payload = errorMessage.getPayload();
assertThat(payload, instanceOf(MessageHandlingException.class));
@@ -196,6 +209,48 @@ public class ChannelSecurityInterceptorSecuredChannelAnnotationTests {
instanceOf(AuthenticationCredentialsNotFoundException.class));
}
@Test
public void testSecurityContextPropagationPublishSubscribeChannel() {
login("bob", "bobspassword", "ROLE_ADMIN", "ROLE_PRESIDENT");
this.publishSubscribeChannel.send(new GenericMessage<String>("test"));
Message<?> receive = this.securedChannelQueue.receive(10000);
assertNotNull(receive);
IntegrationMessageHeaderAccessor headerAccessor = new IntegrationMessageHeaderAccessor(receive);
assertEquals(new Integer(0), headerAccessor.getSequenceNumber());
receive = this.securedChannelQueue2.receive(10000);
assertNotNull(receive);
headerAccessor = new IntegrationMessageHeaderAccessor(receive);
assertEquals(new Integer(0), headerAccessor.getSequenceNumber());
this.publishSubscribeChannel.setApplySequence(true);
this.publishSubscribeChannel.send(new GenericMessage<String>("test"));
receive = this.securedChannelQueue.receive(10000);
assertNotNull(receive);
headerAccessor = new IntegrationMessageHeaderAccessor(receive);
assertEquals(new Integer(1), headerAccessor.getSequenceNumber());
receive = this.securedChannelQueue2.receive(10000);
assertNotNull(receive);
headerAccessor = new IntegrationMessageHeaderAccessor(receive);
assertEquals(new Integer(2), headerAccessor.getSequenceNumber());
this.publishSubscribeChannel.setApplySequence(false);
SecurityContextHolder.clearContext();
this.publishSubscribeChannel.send(new GenericMessage<String>("test"));
Message<?> errorMessage = this.errorChannel.receive(10000);
assertNotNull(errorMessage);
Object payload = errorMessage.getPayload();
assertThat(payload, instanceOf(MessageHandlingException.class));
assertThat(((MessageHandlingException) payload).getCause(),
instanceOf(AuthenticationCredentialsNotFoundException.class));
}
private void login(String username, String password, String... roles) {
SecurityContext context = SecurityTestUtils.createContext(username, password, roles);
@@ -231,7 +286,10 @@ public class ChannelSecurityInterceptorSecuredChannelAnnotationTests {
}
@Bean
@GlobalChannelInterceptor(patterns = {"#{'queueChannel'}", "${security.channel:executorChannel}"})
@GlobalChannelInterceptor(patterns = {
"#{'queueChannel'}",
"${security.channel:executorChannel}",
"publishSubscribeChannel" })
public ChannelInterceptor securityContextPropagationInterceptor() {
return new SecurityContextPropagationChannelInterceptor();
}
@@ -255,6 +313,35 @@ public class ChannelSecurityInterceptorSecuredChannelAnnotationTests {
}
@Bean
public PublishSubscribeChannel publishSubscribeChannel() {
return new PublishSubscribeChannel(Executors.newCachedThreadPool());
}
@Bean
@ServiceActivator(inputChannel = "publishSubscribeChannel")
public MessageHandler securedChannelQueueBridge() {
BridgeHandler handler = new BridgeHandler();
handler.setOutputChannel(securedChannelQueue());
handler.setOrder(1);
return handler;
}
@Bean
@SecuredChannel(interceptor = "channelSecurityInterceptor", sendAccess = {"ROLE_ADMIN", "ROLE_PRESIDENT"})
public PollableChannel securedChannelQueue2() {
return new QueueChannel();
}
@Bean
@ServiceActivator(inputChannel = "publishSubscribeChannel")
public MessageHandler securedChannelQueue2Bridge() {
BridgeHandler handler = new BridgeHandler();
handler.setOutputChannel(securedChannelQueue2());
handler.setOrder(2);
return handler;
}
@Bean
public TaskScheduler taskScheduler() {
return new ThreadPoolTaskScheduler();