Skip to content

Commit

Permalink
add SNS internal endpoint to fetch SubscriptionTokens (localstack#9336)
Browse files Browse the repository at this point in the history
  • Loading branch information
bentsku authored Oct 12, 2023
1 parent b569aa4 commit 83c8550
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 4 deletions.
1 change: 1 addition & 0 deletions localstack/services/sns/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,6 @@
# Endpoint to access all the PlatformEndpoint sent Messages
PLATFORM_ENDPOINT_MSGS_ENDPOINT = "/_aws/sns/platform-endpoint-messages"
SMS_MSGS_ENDPOINT = "/_aws/sns/sms-messages"
SUBSCRIPTION_TOKENS_ENDPOINT = "/_aws/sns/subscription-tokens"

DUMMY_SUBSCRIPTION_PRINCIPAL = "arn:aws:iam::{{account_id}}:user/DummySNSPrincipal"
45 changes: 45 additions & 0 deletions localstack/services/sns/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -897,6 +897,7 @@ def register_sns_api_resource(router: Router):
"""Register the retrospection endpoints as internal LocalStack endpoints."""
router.add(SNSServicePlatformEndpointMessagesApiResource())
router.add(SNSServiceSMSMessagesApiResource())
router.add(SNSServiceSubscriptionTokenApiResource())


def _format_messages(sent_messages: List[Dict[str, str]], validated_keys: List[str]):
Expand Down Expand Up @@ -1041,3 +1042,47 @@ def on_delete(self, request: Request) -> Response:

store.sms_messages.clear()
return Response("", status=204)


class SNSServiceSubscriptionTokenApiResource:
"""Provides a REST API for retrospective access to Subscription Confirmation Tokens to confirm subscriptions.
Those are not sent for email, and sometimes inaccessible when working with external HTTPS endpoint which won't be
able to reach your local host.
This is registered as a LocalStack internal HTTP resource.
This endpoint has the following parameter:
- GET `subscription_arn`: `subscriptionArn`resource in SNS for which you want the SubscriptionToken
"""

@route(f"{sns_constants.SUBSCRIPTION_TOKENS_ENDPOINT}/<path:subscription_arn>", methods=["GET"])
def on_get(self, _request: Request, subscription_arn: str):
try:
parsed_arn = parse_arn(subscription_arn)
except InvalidArnException:
response = Response("", 400)
response.set_json(
{
"error": "The provided SubscriptionARN is invalid",
"subscription_arn": subscription_arn,
}
)
return response

store: SnsStore = sns_stores[parsed_arn["account"]][parsed_arn["region"]]

for token, sub_arn in store.subscription_tokens.items():
if sub_arn == subscription_arn:
return {
"subscription_token": token,
"subscription_arn": subscription_arn,
}

response = Response("", 404)
response.set_json(
{
"error": "The provided SubscriptionARN is not found",
"subscription_arn": subscription_arn,
}
)
return response
70 changes: 68 additions & 2 deletions tests/aws/services/sns/test_sns.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@
TEST_AWS_ACCOUNT_ID,
TEST_AWS_REGION_NAME,
)
from localstack.services.sns.constants import PLATFORM_ENDPOINT_MSGS_ENDPOINT, SMS_MSGS_ENDPOINT
from localstack.services.sns.constants import (
PLATFORM_ENDPOINT_MSGS_ENDPOINT,
SMS_MSGS_ENDPOINT,
SUBSCRIPTION_TOKENS_ENDPOINT,
)
from localstack.services.sns.provider import SnsProvider
from localstack.testing.aws.util import is_aws_cloud
from localstack.testing.pytest import markers
Expand Down Expand Up @@ -3651,7 +3655,8 @@ def handler(_request):
@markers.snapshot.skip_snapshot_verify(
paths=[
"$.http-message-headers.Accept", # requests adds the header but not SNS, not very important
"$.http-message-headers-raw.Accept", # requests adds the header but not SNS, not very important
"$.http-message-headers-raw.Accept",
"$.http-confirm-sub-headers.Accept",
]
)
def test_subscribe_external_http_endpoint(
Expand Down Expand Up @@ -3703,6 +3708,8 @@ def _clean_headers(response_headers: dict):
assert "Signature" in payload
assert "SigningCertURL" in payload

snapshot.match("http-confirm-sub-headers", _clean_headers(sub_request.headers))

token = payload["Token"]
subscribe_url = payload["SubscribeURL"]
service_url, subscribe_url_path = payload["SubscribeURL"].rsplit("/", maxsplit=1)
Expand Down Expand Up @@ -4529,3 +4536,62 @@ def check_message():
assert delete_res.status_code == 204
msg_with_region = requests.get(msgs_url, params={"region": "us-east-1"}).json()
assert not msg_with_region["sms_messages"]

@markers.aws.only_localstack
def test_subscription_tokens_can_retrospect(
self, sns_create_topic, sns_subscription, sns_create_http_endpoint, aws_client
):
sns_store = SnsProvider.get_store(TEST_AWS_ACCOUNT_ID, TEST_AWS_REGION_NAME)
# clean up the saved tokens
sns_store.subscription_tokens.clear()

message = "Good news everyone!"
# Necessitate manual set up to allow external access to endpoint, only in local testing
topic_arn, subscription_arn, endpoint_url, server = sns_create_http_endpoint()
assert poll_condition(
lambda: len(server.log) >= 1,
timeout=5,
)
sub_request, _ = server.log[0]
payload = sub_request.get_json(force=True)
assert payload["Type"] == "SubscriptionConfirmation"
token = payload["Token"]
server.clear()

# we won't confirm the subscription, to simulate an external provider that wouldn't be able to access LocalStack
# try to access the internal to confirm the Token is there
tokens_base_url = config.get_edge_url() + SUBSCRIPTION_TOKENS_ENDPOINT
api_contents = requests.get(f"{tokens_base_url}/{subscription_arn}").json()
assert api_contents["subscription_token"] == token
assert api_contents["subscription_arn"] == subscription_arn

# try to send a message to an unconfirmed subscription, assert that the message isn't received
aws_client.sns.publish(Message=message, TopicArn=topic_arn)

assert poll_condition(
lambda: len(server.log) == 0,
timeout=1,
)

aws_client.sns.confirm_subscription(TopicArn=topic_arn, Token=token)
aws_client.sns.publish(Message=message, TopicArn=topic_arn)
assert poll_condition(
lambda: len(server.log) == 1,
timeout=2,
)

wrong_sub_arn = subscription_arn.replace(TEST_AWS_REGION_NAME, "us-west-1")
wrong_region_req = requests.get(f"{tokens_base_url}/{wrong_sub_arn}")
assert wrong_region_req.status_code == 404
assert wrong_region_req.json() == {
"error": "The provided SubscriptionARN is not found",
"subscription_arn": wrong_sub_arn,
}

# Ensure proper error is raised with wrong ARN
incorrect_arn_req = requests.get(f"{tokens_base_url}/randomarnhere")
assert incorrect_arn_req.status_code == 400
assert incorrect_arn_req.json() == {
"error": "The provided SubscriptionARN is invalid",
"subscription_arn": "randomarnhere",
}
26 changes: 24 additions & 2 deletions tests/aws/services/sns/test_sns.snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -4284,7 +4284,7 @@
}
},
"tests/aws/services/sns/test_sns.py::TestSNSSubscriptionHttp::test_subscribe_external_http_endpoint[True]": {
"recorded-date": "09-10-2023, 16:01:30",
"recorded-date": "12-10-2023, 00:47:24",
"recorded-content": {
"subscription-confirmation": {
"Message": "You have chosen to subscribe to the topic arn:aws:sns:<region>:111111111111:<resource:1>.\nTo confirm the subscription, visit the SubscribeURL included in this message.",
Expand All @@ -4298,6 +4298,17 @@
"TopicArn": "arn:aws:sns:<region>:111111111111:<resource:1>",
"Type": "SubscriptionConfirmation"
},
"http-confirm-sub-headers": {
"Accept-Encoding": "gzip,deflate",
"Connection": "connection",
"Content-Length": "content--length",
"Content-Type": "text/plain; charset=UTF-8",
"Host": "<host:1>",
"User-Agent": "Amazon Simple Notification Service Agent",
"X-Amz-Sns-Message-Id": "<uuid:1>",
"X-Amz-Sns-Message-Type": "SubscriptionConfirmation",
"X-Amz-Sns-Topic-Arn": "arn:aws:sns:<region>:111111111111:<resource:1>"
},
"broken-topic-arn-confirm": {
"ErrorResponse": {
"Error": {
Expand Down Expand Up @@ -4386,7 +4397,7 @@
}
},
"tests/aws/services/sns/test_sns.py::TestSNSSubscriptionHttp::test_subscribe_external_http_endpoint[False]": {
"recorded-date": "09-10-2023, 16:01:39",
"recorded-date": "12-10-2023, 00:47:29",
"recorded-content": {
"subscription-confirmation": {
"Message": "You have chosen to subscribe to the topic arn:aws:sns:<region>:111111111111:<resource:1>.\nTo confirm the subscription, visit the SubscribeURL included in this message.",
Expand All @@ -4400,6 +4411,17 @@
"TopicArn": "arn:aws:sns:<region>:111111111111:<resource:1>",
"Type": "SubscriptionConfirmation"
},
"http-confirm-sub-headers": {
"Accept-Encoding": "gzip,deflate",
"Connection": "connection",
"Content-Length": "content--length",
"Content-Type": "text/plain; charset=UTF-8",
"Host": "<host:1>",
"User-Agent": "Amazon Simple Notification Service Agent",
"X-Amz-Sns-Message-Id": "<uuid:1>",
"X-Amz-Sns-Message-Type": "SubscriptionConfirmation",
"X-Amz-Sns-Topic-Arn": "arn:aws:sns:<region>:111111111111:<resource:1>"
},
"broken-topic-arn-confirm": {
"ErrorResponse": {
"Error": {
Expand Down

0 comments on commit 83c8550

Please sign in to comment.