SNS: Cross-account access for topics (#6330)
This commit is contained in:
parent
b853593259
commit
8ca9c17e5e
@ -4,7 +4,6 @@ import re
|
||||
import json
|
||||
import sys
|
||||
import warnings
|
||||
from collections import namedtuple
|
||||
from datetime import datetime
|
||||
from enum import Enum, unique
|
||||
from json import JSONDecodeError
|
||||
@ -27,6 +26,7 @@ from moto.events.exceptions import (
|
||||
IllegalStatusException,
|
||||
)
|
||||
from moto.moto_api._internal import mock_random as random
|
||||
from moto.utilities.arns import parse_arn
|
||||
from moto.utilities.paginator import paginate
|
||||
from moto.utilities.tagging_service import TaggingService
|
||||
|
||||
@ -37,10 +37,6 @@ UNDEFINED = object()
|
||||
|
||||
|
||||
class Rule(CloudFormationModel):
|
||||
Arn = namedtuple(
|
||||
"Arn", ["account", "region", "service", "resource_type", "resource_id"]
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
@ -123,13 +119,13 @@ class Rule(CloudFormationModel):
|
||||
# - SQS Queue + FIFO Queue
|
||||
# - Cross-region/account EventBus
|
||||
for target in self.targets:
|
||||
arn = self._parse_arn(target["Arn"])
|
||||
arn = parse_arn(target["Arn"])
|
||||
|
||||
if arn.service == "logs" and arn.resource_type == "log-group":
|
||||
self._send_to_cw_log_group(arn.resource_id, event)
|
||||
elif arn.service == "events" and not arn.resource_type:
|
||||
input_template = json.loads(target["InputTransformer"]["InputTemplate"])
|
||||
archive_arn = self._parse_arn(input_template["archive-arn"])
|
||||
archive_arn = parse_arn(input_template["archive-arn"])
|
||||
|
||||
self._send_to_events_archive(archive_arn.resource_id, event)
|
||||
elif arn.service == "sqs":
|
||||
@ -149,33 +145,6 @@ class Rule(CloudFormationModel):
|
||||
else:
|
||||
raise NotImplementedError(f"Expr not defined for {type(self)}")
|
||||
|
||||
def _parse_arn(self, arn: str) -> Arn:
|
||||
# http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html
|
||||
# this method needs probably some more fine tuning,
|
||||
# when also other targets are supported
|
||||
_, _, service, region, account, resource = arn.split(":", 5)
|
||||
|
||||
if ":" in resource and "/" in resource:
|
||||
if resource.index(":") < resource.index("/"):
|
||||
resource_type, resource_id = resource.split(":", 1)
|
||||
else:
|
||||
resource_type, resource_id = resource.split("/", 1)
|
||||
elif ":" in resource:
|
||||
resource_type, resource_id = resource.split(":", 1)
|
||||
elif "/" in resource:
|
||||
resource_type, resource_id = resource.split("/", 1)
|
||||
else:
|
||||
resource_type = None
|
||||
resource_id = resource
|
||||
|
||||
return self.Arn(
|
||||
account=account,
|
||||
region=region,
|
||||
service=service,
|
||||
resource_type=resource_type,
|
||||
resource_id=resource_id,
|
||||
)
|
||||
|
||||
def _send_to_cw_log_group(self, name: str, event: Dict[str, Any]) -> None:
|
||||
from moto.logs import logs_backends
|
||||
|
||||
|
@ -34,7 +34,7 @@ from .utils import (
|
||||
is_e164,
|
||||
FilterPolicyMatcher,
|
||||
)
|
||||
|
||||
from moto.utilities.arns import parse_arn
|
||||
|
||||
DEFAULT_PAGE_SIZE = 100
|
||||
MAXIMUM_MESSAGE_LENGTH = 262144 # 256 KiB
|
||||
@ -486,13 +486,15 @@ class SNSBackend(BaseBackend):
|
||||
try:
|
||||
topic = self.get_topic(arn)
|
||||
self.delete_topic_subscriptions(topic)
|
||||
self.topics.pop(arn)
|
||||
parsed_arn = parse_arn(arn)
|
||||
sns_backends[parsed_arn.account][parsed_arn.region].topics.pop(arn, None)
|
||||
except KeyError:
|
||||
raise SNSNotFoundError(f"Topic with arn {arn} not found")
|
||||
|
||||
def get_topic(self, arn: str) -> Topic:
|
||||
parsed_arn = parse_arn(arn)
|
||||
try:
|
||||
return self.topics[arn]
|
||||
return sns_backends[parsed_arn.account][parsed_arn.region].topics[arn]
|
||||
except KeyError:
|
||||
raise SNSNotFoundError(f"Topic with arn {arn} not found")
|
||||
|
||||
@ -932,10 +934,8 @@ class SNSBackend(BaseBackend):
|
||||
aws_account_ids: List[str],
|
||||
action_names: List[str],
|
||||
) -> None:
|
||||
if topic_arn not in self.topics:
|
||||
raise SNSNotFoundError("Topic does not exist")
|
||||
|
||||
policy = self.topics[topic_arn]._policy_json
|
||||
topic = self.get_topic(topic_arn)
|
||||
policy = topic._policy_json
|
||||
statement = next(
|
||||
(
|
||||
statement
|
||||
@ -964,18 +964,16 @@ class SNSBackend(BaseBackend):
|
||||
"Resource": topic_arn,
|
||||
}
|
||||
|
||||
self.topics[topic_arn]._policy_json["Statement"].append(statement)
|
||||
topic._policy_json["Statement"].append(statement)
|
||||
|
||||
def remove_permission(self, topic_arn: str, label: str) -> None:
|
||||
if topic_arn not in self.topics:
|
||||
raise SNSNotFoundError("Topic does not exist")
|
||||
|
||||
statements = self.topics[topic_arn]._policy_json["Statement"]
|
||||
topic = self.get_topic(topic_arn)
|
||||
statements = topic._policy_json["Statement"]
|
||||
statements = [
|
||||
statement for statement in statements if statement["Sid"] != label
|
||||
]
|
||||
|
||||
self.topics[topic_arn]._policy_json["Statement"] = statements
|
||||
topic._policy_json["Statement"] = statements
|
||||
|
||||
def list_tags_for_resource(self, resource_arn: str) -> Dict[str, str]:
|
||||
if resource_arn not in self.topics:
|
||||
|
33
moto/utilities/arns.py
Normal file
33
moto/utilities/arns.py
Normal file
@ -0,0 +1,33 @@
|
||||
from collections import namedtuple
|
||||
|
||||
Arn = namedtuple(
|
||||
"Arn", ["account", "region", "service", "resource_type", "resource_id"]
|
||||
)
|
||||
|
||||
|
||||
def parse_arn(arn: str) -> Arn:
|
||||
# http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html
|
||||
# this method needs probably some more fine tuning,
|
||||
# when also other targets are supported
|
||||
_, _, service, region, account, resource = arn.split(":", 5)
|
||||
|
||||
if ":" in resource and "/" in resource:
|
||||
if resource.index(":") < resource.index("/"):
|
||||
resource_type, resource_id = resource.split(":", 1)
|
||||
else:
|
||||
resource_type, resource_id = resource.split("/", 1)
|
||||
elif ":" in resource:
|
||||
resource_type, resource_id = resource.split(":", 1)
|
||||
elif "/" in resource:
|
||||
resource_type, resource_id = resource.split("/", 1)
|
||||
else:
|
||||
resource_type = None
|
||||
resource_id = resource
|
||||
|
||||
return Arn(
|
||||
account=account,
|
||||
region=region,
|
||||
service=service,
|
||||
resource_type=resource_type,
|
||||
resource_id=resource_id,
|
||||
)
|
@ -96,9 +96,9 @@ def test_create_topic_should_be_indempodent():
|
||||
@mock_sns
|
||||
def test_get_missing_topic():
|
||||
conn = boto3.client("sns", region_name="us-east-1")
|
||||
conn.get_topic_attributes.when.called_with(TopicArn="a-fake-arn").should.throw(
|
||||
ClientError
|
||||
)
|
||||
conn.get_topic_attributes.when.called_with(
|
||||
TopicArn="arn:aws:sns:us-east-1:424242424242:a-fake-arn"
|
||||
).should.throw(ClientError)
|
||||
|
||||
|
||||
@mock_sns
|
||||
@ -360,7 +360,10 @@ def test_add_permission_errors():
|
||||
Label="test-2",
|
||||
AWSAccountId=["999999999999"],
|
||||
ActionName=["AddPermission"],
|
||||
).should.throw(ClientError, "Topic does not exist")
|
||||
).should.throw(
|
||||
ClientError,
|
||||
f"An error occurred (NotFound) when calling the AddPermission operation: Topic with arn {topic_arn + '-not-existing'} not found",
|
||||
)
|
||||
|
||||
client.add_permission.when.called_with(
|
||||
TopicArn=topic_arn,
|
||||
@ -383,7 +386,10 @@ def test_remove_permission_errors():
|
||||
|
||||
client.remove_permission.when.called_with(
|
||||
TopicArn=topic_arn + "-not-existing", Label="test"
|
||||
).should.throw(ClientError, "Topic does not exist")
|
||||
).should.throw(
|
||||
ClientError,
|
||||
f"An error occurred (NotFound) when calling the RemovePermission operation: Topic with arn {topic_arn + '-not-existing'} not found",
|
||||
)
|
||||
|
||||
|
||||
@mock_sns
|
||||
|
Loading…
Reference in New Issue
Block a user