Skip to content
This repository was archived by the owner on Mar 23, 2026. It is now read-only.

Commit 4246fb5

Browse files
committed
add operations, add tests
1 parent fe9daa5 commit 4246fb5

File tree

5 files changed

+411
-0
lines changed

5 files changed

+411
-0
lines changed

localstack-core/localstack/services/sns/constants.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,19 @@
2525
"SubscriptionRoleArn",
2626
]
2727

28+
29+
VALID_POLICY_ACTIONS = [
30+
"GetTopicAttributes",
31+
"SetTopicAttributes",
32+
"AddPermission",
33+
"RemovePermission",
34+
"DeleteTopic",
35+
"Subscribe",
36+
"ListSubscriptionsByTopic",
37+
"Publish",
38+
"Receive",
39+
]
40+
2841
MSG_ATTR_NAME_REGEX = re.compile(r"^(?!\.)(?!.*\.$)(?!.*\.\.)[a-zA-Z0-9_\-.]+$")
2942
ATTR_TYPE_REGEX = re.compile(r"^(String|Number|Binary)\..+$")
3043
VALID_MSG_ATTR_NAME_CHARS = set(ascii_letters + digits + "." + "-" + "_")

localstack-core/localstack/services/sns/v2/provider.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@
1010

1111
from localstack.aws.api import CommonServiceException, RequestContext
1212
from localstack.aws.api.sns import (
13+
ActionsList,
1314
AmazonResourceName,
1415
BatchEntryIdsNotDistinctException,
1516
CheckIfPhoneNumberIsOptedOutResponse,
1617
ConfirmSubscriptionResponse,
1718
CreateEndpointResponse,
1819
CreatePlatformApplicationResponse,
1920
CreateTopicResponse,
21+
DelegatesList,
2022
Endpoint,
2123
EndpointDisabledException,
2224
GetEndpointAttributesResponse,
@@ -60,6 +62,7 @@
6062
attributeValue,
6163
authenticateOnUnsubscribe,
6264
endpoint,
65+
label,
6366
message,
6467
messageStructure,
6568
nextToken,
@@ -88,6 +91,7 @@
8891
SUBSCRIPTION_TOKENS_ENDPOINT,
8992
VALID_APPLICATION_PLATFORMS,
9093
VALID_MSG_ATTR_NAME_CHARS,
94+
VALID_POLICY_ACTIONS,
9195
VALID_SUBSCRIPTION_ATTR_NAME,
9296
)
9397
from localstack.services.sns.filter import FilterPolicyValidator
@@ -1114,6 +1118,61 @@ def opt_in_phone_number(
11141118
store.PHONE_NUMBERS_OPTED_OUT.remove(phone_number)
11151119
return OptInPhoneNumberResponse()
11161120

1121+
#
1122+
# Permission operations
1123+
#
1124+
1125+
def add_permission(
1126+
self,
1127+
context: RequestContext,
1128+
topic_arn: topicARN,
1129+
label: label,
1130+
aws_account_id: DelegatesList,
1131+
action_name: ActionsList,
1132+
**kwargs,
1133+
) -> None:
1134+
topic: Topic = self._get_topic(topic_arn, context)
1135+
policy = json.loads(topic["attributes"]["Policy"])
1136+
statement = next(
1137+
(statement for statement in policy["Statement"] if statement["Sid"] == label),
1138+
None,
1139+
)
1140+
1141+
if statement:
1142+
raise InvalidParameterException("Invalid parameter: Statement already exists")
1143+
1144+
if any(action not in VALID_POLICY_ACTIONS for action in action_name):
1145+
raise InvalidParameterException(
1146+
"Invalid parameter: Policy statement action out of service scope!"
1147+
)
1148+
1149+
principals = [
1150+
f"arn:{get_partition(context.region)}:iam::{account_id}:root"
1151+
for account_id in aws_account_id
1152+
]
1153+
actions = [f"SNS:{action}" for action in action_name]
1154+
1155+
statement = {
1156+
"Sid": label,
1157+
"Effect": "Allow",
1158+
"Principal": {"AWS": principals[0] if len(principals) == 1 else principals},
1159+
"Action": actions[0] if len(actions) == 1 else actions,
1160+
"Resource": topic_arn,
1161+
}
1162+
1163+
policy["Statement"].append(statement)
1164+
topic["attributes"]["Policy"] = json.dumps(policy)
1165+
1166+
def remove_permission(
1167+
self, context: RequestContext, topic_arn: topicARN, label: label, **kwargs
1168+
) -> None:
1169+
topic = self._get_topic(topic_arn, context)
1170+
policy = json.loads(topic["attributes"]["Policy"])
1171+
statements = policy["Statement"]
1172+
statements = [statement for statement in statements if statement["Sid"] != label]
1173+
policy["Statement"] = statements
1174+
topic["attributes"]["Policy"] = json.dumps(policy)
1175+
11171176
def list_tags_for_resource(
11181177
self, context: RequestContext, resource_arn: AmazonResourceName, **kwargs
11191178
) -> ListTagsForResourceResponse:

tests/aws/services/sns/test_sns.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,107 @@ def test_topic_get_attributes_with_fifo_false(self, sns_create_topic, aws_client
517517
)
518518
snapshot.match("set-fifo-false-after-creation", e.value.response)
519519

520+
@markers.aws.validated
521+
def test_topic_add_permission(self, sns_create_topic, aws_client, snapshot, account_id):
522+
topic_arn = sns_create_topic()["TopicArn"]
523+
resp = aws_client.sns.add_permission(
524+
TopicArn=topic_arn, Label="test", AWSAccountId=[account_id], ActionName=["Publish"]
525+
)
526+
snapshot.match("add-permission-response", resp)
527+
528+
attributes_resp = aws_client.sns.get_topic_attributes(TopicArn=topic_arn)
529+
policy_str = attributes_resp["Attributes"]["Policy"]
530+
policy_json = json.loads(policy_str)
531+
snapshot.match("topic-policy-after-permission", policy_json)
532+
533+
@markers.aws.validated
534+
def test_topic_add_multiple_permissions(
535+
self, sns_create_topic, aws_client, snapshot, account_id
536+
):
537+
topic_arn = sns_create_topic()["TopicArn"]
538+
resp = aws_client.sns.add_permission(
539+
TopicArn=topic_arn,
540+
Label="test",
541+
AWSAccountId=[account_id],
542+
ActionName=["Publish", "Subscribe"],
543+
)
544+
snapshot.match("add-permission-response", resp)
545+
546+
attributes_resp = aws_client.sns.get_topic_attributes(TopicArn=topic_arn)
547+
policy_str = attributes_resp["Attributes"]["Policy"]
548+
policy_json = json.loads(policy_str)
549+
snapshot.match("topic-policy-after-permission", policy_json)
550+
551+
@markers.aws.validated
552+
def test_topic_remove_permission(self, sns_create_topic, aws_client, snapshot, account_id):
553+
topic_arn = sns_create_topic()["TopicArn"]
554+
label = "test"
555+
aws_client.sns.add_permission(
556+
TopicArn=topic_arn, Label=label, AWSAccountId=[account_id], ActionName=["Publish"]
557+
)
558+
559+
aws_client.sns.remove_permission(TopicArn=topic_arn, Label=label)
560+
attributes_resp = aws_client.sns.get_topic_attributes(TopicArn=topic_arn)
561+
policy_str = attributes_resp["Attributes"]["Policy"]
562+
policy_json = json.loads(policy_str)
563+
snapshot.match("topic-policy", policy_json)
564+
565+
@markers.snapshot.skip_snapshot_verify(paths=["$..Error.Message"], condition=is_sns_v1_provider)
566+
@markers.aws.validated
567+
def test_add_permission_errors(self, snapshot, aws_client, account_id):
568+
topic_name = f"topic-{short_uid()}"
569+
topic_arn = aws_client.sns.create_topic(Name=topic_name)["TopicArn"]
570+
571+
aws_client.sns.add_permission(
572+
TopicArn=topic_arn,
573+
Label="test",
574+
AWSAccountId=[account_id],
575+
ActionName=["Publish"],
576+
)
577+
578+
with pytest.raises(ClientError) as e:
579+
aws_client.sns.add_permission(
580+
TopicArn=topic_arn,
581+
Label="test",
582+
AWSAccountId=[account_id],
583+
ActionName=["AddPermission"],
584+
)
585+
snapshot.match("duplicate-label", e.value.response)
586+
587+
with pytest.raises(ClientError) as e:
588+
aws_client.sns.add_permission(
589+
TopicArn=f"{topic_arn}-not-existing",
590+
Label="test-2",
591+
AWSAccountId=[account_id],
592+
ActionName=["AddPermission"],
593+
)
594+
snapshot.match("topic-not-found", e.value.response)
595+
596+
with pytest.raises(ClientError) as e:
597+
aws_client.sns.add_permission(
598+
TopicArn=topic_arn,
599+
Label="test-2",
600+
AWSAccountId=[account_id],
601+
ActionName=["InvalidAction"],
602+
)
603+
snapshot.match("invalid-action", e.value.response)
604+
605+
@markers.aws.validated
606+
def test_remove_permission_errors(self, snapshot, aws_client, account_id):
607+
topic_name = f"topic-{short_uid()}"
608+
topic_arn = aws_client.sns.create_topic(Name=topic_name)["TopicArn"]
609+
aws_client.sns.add_permission(
610+
TopicArn=topic_arn,
611+
Label="test",
612+
AWSAccountId=[account_id],
613+
ActionName=["Publish"],
614+
)
615+
616+
with pytest.raises(ClientError) as e:
617+
aws_client.sns.remove_permission(TopicArn=f"{topic_arn}-not-existing", Label="test")
618+
619+
snapshot.match("topic-not-found", e.value.response)
620+
520621

521622
class TestSNSPublishCrud:
522623
"""

0 commit comments

Comments
 (0)