Skip to content

Commit

Permalink
Verify SNS messages in HTTP subscription mode (#240)
Browse files Browse the repository at this point in the history
Fixes #176
  • Loading branch information
MatejNedic authored Feb 15, 2022
1 parent f1692dd commit 8a605b6
Show file tree
Hide file tree
Showing 18 changed files with 188 additions and 28 deletions.
1 change: 1 addition & 0 deletions docs/src/main/asciidoc/_configprops.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
|cloud.aws.sns.enabled | `true` | Enables SNS integration.
|cloud.aws.sns.endpoint | |
|cloud.aws.sns.region | |
|cloud.aws.sns.verification | `true` | Defines if SNS massages will be verified. By default, verification is used.
|cloud.aws.sqs.enabled | `true` | Enables SQS integration.
|cloud.aws.sqs.endpoint | |
|cloud.aws.sqs.handler.default-deletion-policy | | Configures global deletion policy used if deletion policy is not explicitly set on {@link SqsListener}.
Expand Down
8 changes: 8 additions & 0 deletions docs/src/main/asciidoc/sns.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,14 @@ SNS sends three type of requests to an HTTP topic listener endpoint, for each of
* Notification request -> `@NotificationMessageMapping`
* Unsubscription request -> `@NotificationUnsubscribeMapping`

[NOTE]
====
Since 2.4.0 verification has been introduced for Notification request and is turned on by default. Verification is using same region as SNSClient is.
To turn off verification simply set property 'cloud.aws.sns.verification=false'.
With Localstack verification won't work, so you have to set property to 'false' if you want `@NotificationMessageMapping` to work properly.
For more information about SNS verification https://docs.awspring.io/spring-cloud-aws/docs/2.3.3/reference/html/index.html [here].
====

HTTP endpoints are based on Spring MVC controllers. Spring Cloud AWS added some custom argument resolvers to extract
the message and subject out of the notification requests.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,22 @@

import com.amazonaws.ClientConfiguration;
import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.sns.AmazonSNS;
import com.amazonaws.services.sns.AmazonSNSClient;
import com.amazonaws.services.sns.message.SnsMessageManager;
import io.awspring.cloud.context.annotation.ConditionalOnMissingAmazonClient;
import io.awspring.cloud.core.config.AmazonWebserviceClientFactoryBean;
import io.awspring.cloud.core.region.RegionProvider;
import io.awspring.cloud.core.region.StaticRegionProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
Expand All @@ -49,13 +54,17 @@
* @author Alain Sahli
* @author Eddú Meléndez
* @author Maciej Walkowiak
* @author Manuel Wessner
* @author Matej Nedic
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(AmazonSNS.class)
@EnableConfigurationProperties(SnsProperties.class)
@ConditionalOnProperty(name = "cloud.aws.sns.enabled", havingValue = "true", matchIfMissing = true)
public class SnsAutoConfiguration {

private static final Logger LOGGER = LoggerFactory.getLogger(SnsAutoConfiguration.class);

private final AWSCredentialsProvider awsCredentialsProvider;

private final RegionProvider regionProvider;
Expand All @@ -81,16 +90,32 @@ public AmazonWebserviceClientFactoryBean<AmazonSNSClient> amazonSNS(SnsPropertie
return clientFactoryBean;
}

@ConditionalOnProperty(name = "cloud.aws.sns.verification", havingValue = "true", matchIfMissing = true)
@ConditionalOnMissingBean(SnsMessageManager.class)
@Bean
public SnsMessageManager snsMessageManager(SnsProperties snsProperties) {
if (regionProvider == null) {
String defaultRegion = Regions.DEFAULT_REGION.getName();
LOGGER.warn(
"RegionProvider bean not configured. Configuring SnsMessageManager with region " + defaultRegion);
return new SnsMessageManager(defaultRegion);
}
else {
return new SnsMessageManager(regionProvider.getRegion().getName());
}
}

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(WebMvcConfigurer.class)
static class SnsWebConfiguration {

@Bean
public WebMvcConfigurer snsWebMvcConfigurer(AmazonSNS amazonSns) {
public WebMvcConfigurer snsWebMvcConfigurer(AmazonSNS amazonSns,
Optional<SnsMessageManager> snsMessageManager) {
return new WebMvcConfigurer() {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(getNotificationHandlerMethodArgumentResolver(amazonSns));
resolvers.add(getNotificationHandlerMethodArgumentResolver(amazonSns, snsMessageManager));
}
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,17 @@
@ConfigurationProperties(prefix = "cloud.aws.sns")
public class SnsProperties extends AwsClientProperties {

/**
* Defines if SNS massages will be verified. By default, verification is used.
*/
private boolean verification = true;

public boolean getVerification() {
return verification;
}

public void setVerification(boolean verification) {
this.verification = verification;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import com.amazonaws.regions.Regions;
import com.amazonaws.services.sns.AmazonSNS;
import com.amazonaws.services.sns.AmazonSNSClient;
import com.amazonaws.services.sns.message.SnsMessageManager;
import io.awspring.cloud.core.region.RegionProvider;
import io.awspring.cloud.core.region.StaticRegionProvider;
import io.awspring.cloud.messaging.endpoint.NotificationStatusHandlerMethodArgumentResolver;
Expand Down Expand Up @@ -77,6 +78,7 @@ void enableSns_withMinimalConfig_shouldConfigureACompositeArgumentResolver() thr
assertThat(compositeArgumentResolver.getResolvers()).hasSize(3);
assertThat(getNotificationStatusHandlerMethodArgumentResolver(compositeArgumentResolver.getResolvers()))
.hasFieldOrProperty("amazonSns").isNotNull();
assertThat(context).hasSingleBean(SnsMessageManager.class);
});
}

Expand All @@ -89,6 +91,7 @@ void enableSns_withProvidedCredentials_shouldBeUsedToCreateClient() throws Excep
// Assert
assertThat(amazonSns).hasFieldOrPropertyWithValue("awsCredentialsProvider",
SnsConfigurationWithCredentials.AWS_CREDENTIALS_PROVIDER);
assertThat(context).hasSingleBean(SnsMessageManager.class);
});
}

Expand All @@ -97,6 +100,16 @@ void disableSns() {
this.contextRunner.withPropertyValues("cloud.aws.sns.enabled:false").run(context -> {
assertThat(context).doesNotHaveBean(AmazonSNS.class);
assertThat(context).doesNotHaveBean(AmazonSNSClient.class);
assertThat(context).doesNotHaveBean(SnsMessageManager.class);
});
}

@Test
void disableSnsVerification() {
this.contextRunner.withPropertyValues("cloud.aws.sns.verification:false").run(context -> {
assertThat(context).doesNotHaveBean(SnsMessageManager.class);
assertThat(context).hasSingleBean(AmazonSNS.class);
assertThat(context).hasSingleBean(AmazonSNSClient.class);
});
}

Expand All @@ -116,6 +129,7 @@ void enableSns_withCustomAmazonSnsClient_shouldBeUsedByTheArgumentResolver() thr
handlerMethodArgumentResolver.getResolvers());
assertThat(notificationStatusHandlerMethodArgumentResolver).hasFieldOrPropertyWithValue("amazonSns",
SnsConfigurationWithCustomAmazonClient.AMAZON_SNS);
assertThat(context).hasSingleBean(SnsMessageManager.class);
});
}

Expand All @@ -128,6 +142,7 @@ void enableSns_withRegionProvided_shouldBeUsedToCreateClient() throws Exception
// Assert
assertThat(ReflectionTestUtils.getField(amazonSns, "endpoint").toString())
.isEqualTo("https://" + Region.getRegion(Regions.EU_WEST_1).getServiceEndpoint("sns"));
assertThat(context).hasSingleBean(SnsMessageManager.class);
});
}

Expand All @@ -137,6 +152,7 @@ void enableSnsWithSpecificRegion() {
AmazonSNSClient client = context.getBean(AmazonSNSClient.class);
Object region = ReflectionTestUtils.getField(client, "signingRegion");
assertThat(region).isEqualTo(Regions.US_EAST_1.getName());
assertThat(context).hasSingleBean(SnsMessageManager.class);
});
}

Expand All @@ -149,6 +165,7 @@ void enableSnsWithCustomEndpoint() {
assertThat(endpoint).isEqualTo(URI.create("http://localhost:8090"));

Boolean isEndpointOverridden = (Boolean) ReflectionTestUtils.getField(client, "isEndpointOverridden");
assertThat(context).hasSingleBean(SnsMessageManager.class);
assertThat(isEndpointOverridden).isTrue();
});
}
Expand All @@ -162,6 +179,7 @@ void configuration_withGlobalClientConfiguration_shouldUseItForClient() {
// Assert
ClientConfiguration clientConfiguration = (ClientConfiguration) ReflectionTestUtils.getField(amazonSns,
"clientConfiguration");
assertThat(context).hasSingleBean(SnsMessageManager.class);
assertThat(clientConfiguration.getProxyHost()).isEqualTo("global");
});
}
Expand All @@ -175,6 +193,7 @@ void configuration_withSnsClientConfiguration_shouldUseItForClient() {
// Assert
ClientConfiguration clientConfiguration = (ClientConfiguration) ReflectionTestUtils.getField(amazonSns,
"clientConfiguration");
assertThat(context).hasSingleBean(SnsMessageManager.class);
assertThat(clientConfiguration.getProxyHost()).isEqualTo("sns");
});
}
Expand All @@ -189,6 +208,7 @@ void configuration_withGlobalAndSnsClientConfigurations_shouldUseSnsConfiguratio
// Assert
ClientConfiguration clientConfiguration = (ClientConfiguration) ReflectionTestUtils
.getField(amazonSns, "clientConfiguration");
assertThat(context).hasSingleBean(SnsMessageManager.class);
assertThat(clientConfiguration.getProxyHost()).isEqualTo("sns");
});
}
Expand All @@ -198,6 +218,7 @@ void doesNotFailWithoutWebMvcConfigurerOnClasspath() {
this.contextRunner.withUserConfiguration(NoSpringMvcSnsConfiguration.class)
.withClassLoader(new FilteredClassLoader(WebMvcConfigurer.class)).run((context) -> {
assertThat(context).hasSingleBean(AmazonSNS.class);
assertThat(context).hasSingleBean(SnsMessageManager.class);
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
package io.awspring.cloud.messaging.config.annotation;

import java.util.List;
import java.util.Optional;

import com.amazonaws.services.sns.AmazonSNS;
import com.amazonaws.services.sns.message.SnsMessageManager;
import io.awspring.cloud.context.annotation.ConditionalOnClass;

import org.springframework.context.annotation.Bean;
Expand All @@ -37,11 +39,11 @@
public class SnsWebConfiguration {

@Bean
public WebMvcConfigurer snsWebMvcConfigurer(AmazonSNS amazonSns) {
public WebMvcConfigurer snsWebMvcConfigurer(AmazonSNS amazonSns, Optional<SnsMessageManager> snsMessageManager) {
return new WebMvcConfigurer() {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(getNotificationHandlerMethodArgumentResolver(amazonSns));
argumentResolvers.add(getNotificationHandlerMethodArgumentResolver(amazonSns, snsMessageManager));
}
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.util.Arrays;
import java.util.List;

import com.amazonaws.services.sns.message.SnsMessageManager;
import com.fasterxml.jackson.databind.JsonNode;
import io.awspring.cloud.messaging.config.annotation.NotificationMessage;

Expand All @@ -35,21 +36,29 @@
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.messaging.converter.MessageConversionException;
import org.springframework.util.StringUtils;

/**
* @author Agim Emruli
* @author Manuel Wessner
* @author Matej Nedic
*/
public class NotificationMessageHandlerMethodArgumentResolver
extends AbstractNotificationMessageHandlerMethodArgumentResolver {

private final List<HttpMessageConverter<?>> messageConverter;

public NotificationMessageHandlerMethodArgumentResolver() {
this(Arrays.asList(new MappingJackson2HttpMessageConverter(), new StringHttpMessageConverter()));
private final SnsMessageManager snsMessageManager;

public NotificationMessageHandlerMethodArgumentResolver(SnsMessageManager snsMessageManager) {
this(Arrays.asList(new MappingJackson2HttpMessageConverter(), new StringHttpMessageConverter()),
snsMessageManager);
}

public NotificationMessageHandlerMethodArgumentResolver(List<HttpMessageConverter<?>> messageConverter) {
public NotificationMessageHandlerMethodArgumentResolver(List<HttpMessageConverter<?>> messageConverter,
SnsMessageManager snsMessageManager) {
this.snsMessageManager = snsMessageManager;
this.messageConverter = messageConverter;
}

Expand Down Expand Up @@ -81,7 +90,9 @@ protected Object doResolveArgumentFromNotificationMessage(JsonNode content, Http

MediaType mediaType = getMediaType(content);
String messageContent = content.findPath("Message").asText();

if (snsMessageManager != null && content.has("SignatureVersion")) {
verifySignature(content.toString());
}
for (HttpMessageConverter<?> converter : this.messageConverter) {
if (converter.canRead(parameterType, mediaType)) {
try {
Expand All @@ -99,6 +110,16 @@ protected Object doResolveArgumentFromNotificationMessage(JsonNode content, Http
"Error converting notification message with payload:" + messageContent);
}

private void verifySignature(String payload) {
try (InputStream messageStream = new ByteArrayInputStream(payload.getBytes())) {
// Unmarshalling the message is not needed, but also done here
snsMessageManager.parseMessage(messageStream);
}
catch (IOException e) {
throw new MessageConversionException("Issue while verifying signature of Payload: '" + payload + "'", e);
}
}

private static final class ByteArrayHttpInputMessage implements HttpInputMessage {

private final String content;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@

package io.awspring.cloud.messaging.endpoint.config;

import java.util.Optional;

import com.amazonaws.services.sns.AmazonSNS;
import com.amazonaws.services.sns.message.SnsMessageManager;
import io.awspring.cloud.messaging.endpoint.NotificationMessageHandlerMethodArgumentResolver;
import io.awspring.cloud.messaging.endpoint.NotificationStatusHandlerMethodArgumentResolver;
import io.awspring.cloud.messaging.endpoint.NotificationSubjectHandlerMethodArgumentResolver;
Expand All @@ -34,10 +37,16 @@ private NotificationHandlerMethodArgumentResolverConfigurationUtils() {
throw new IllegalStateException("Can't instantiate a utility class");
}

public static HandlerMethodArgumentResolver getNotificationHandlerMethodArgumentResolver(AmazonSNS amazonSns) {
public static HandlerMethodArgumentResolver getNotificationHandlerMethodArgumentResolver(AmazonSNS amazonSns,
Optional<SnsMessageManager> snsMessageManager) {
HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
composite.addResolver(new NotificationStatusHandlerMethodArgumentResolver(amazonSns));
composite.addResolver(new NotificationMessageHandlerMethodArgumentResolver());
if (snsMessageManager.isPresent()) {
composite.addResolver(new NotificationMessageHandlerMethodArgumentResolver(snsMessageManager.get()));
}
else {
composite.addResolver(new NotificationMessageHandlerMethodArgumentResolver(null));
}
composite.addResolver(new NotificationSubjectHandlerMethodArgumentResolver());
return composite;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@

package io.awspring.cloud.messaging.endpoint.config;

import java.util.Optional;

import com.amazonaws.services.sns.AmazonSNS;
import com.amazonaws.services.sns.message.SnsMessageManager;

import org.springframework.beans.factory.config.AbstractFactoryBean;
import org.springframework.util.Assert;
Expand All @@ -33,9 +36,19 @@ public class NotificationHandlerMethodArgumentResolverFactoryBean

private final AmazonSNS amazonSns;

private final SnsMessageManager snsMessageManager;

public NotificationHandlerMethodArgumentResolverFactoryBean(AmazonSNS amazonSns) {
Assert.notNull(amazonSns, "AmazonSns must not be null");
this.amazonSns = null;
snsMessageManager = null;
}

public NotificationHandlerMethodArgumentResolverFactoryBean(AmazonSNS amazonSns,
SnsMessageManager snsMessageManager) {
Assert.notNull(amazonSns, "AmazonSns must not be null");
this.amazonSns = amazonSns;
this.snsMessageManager = snsMessageManager;
}

@Override
Expand All @@ -45,7 +58,7 @@ public Class<HandlerMethodArgumentResolver> getObjectType() {

@Override
protected HandlerMethodArgumentResolver createInstance() throws Exception {
return getNotificationHandlerMethodArgumentResolver(this.amazonSns);
return getNotificationHandlerMethodArgumentResolver(this.amazonSns, Optional.ofNullable(snsMessageManager));
}

}
Loading

0 comments on commit 8a605b6

Please sign in to comment.