Repository: localstack/localstack

Issue:
bug: SNS publish fails if filter policies match a String.Array with true/false/null
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Current Behavior

When we publish some messages, localstack fails while trying to apply filter policies. 

Where the message attribute is a String.Array that contains `true`, `false` or `null` (which is [valid](https://docs.aws.amazon.com/sns/latest/dg/sns-message-attributes.html#SNSMessageAttributes.DataTypes)), localstack doesn't parse the attribute correctly, as it's using `ast.literal_eval`.



### Expected Behavior

The message attribute should be parsed and the filter policy applied, as for string or boolean values.

### How are you starting LocalStack?

With a docker-compose file

### Steps To Reproduce


#### Client commands (e.g., AWS SDK code snippet, or sequence of "awslocal" commands)

```
aws --endpoint-url=http://localhost:4566 sns create-topic --name test-topic
aws --endpoint-url=http://localhost:4566 sqs create-queue --queue-name test-queue

aws --endpoint-url=http://localhost:4566 sns subscribe --topic-arn arn:aws:sns:eu-west-2:000000000000:test-topic --protocol sqs  --notification-endpoint arn:aws:sqs:eu-west-2:000000000000:test-queue

# Taking subscription ARN from results of previous command
aws --endpoint-url=http://localhost:4566 sns set-subscription-attributes --subscription-arn arn:aws:sns:eu-west-2:000000000000:test-topic:726531bf-ef59-405c-823d-b693cf0eaf0a  --attribute-name FilterPolicy --attribute-value '{"HAS_FOO": [true]}'

aws --endpoint-url=http://localhost:4566 sns publish --topic-arn arn:aws:sns:eu-west-2:000000000000:test-topic --message "hello"  --message-attributes '{"HAS_FOO": {"DataType": "String.Array", "StringValue":"[true]"}}'

```


Stack trace from logs

```
master-localstack-1  | 2023-05-11T15:49:34.120682588Z 2023-05-11T15:49:34.120  INFO --- [   asgi_gw_0] localstack.request.aws     : AWS sns.Publish => 500 (InternalError)
master-localstack-1  | 2023-05-11T15:49:34.696141630Z 2023-05-11T15:49:34.695 ERROR --- [   asgi_gw_2] l.aws.handlers.logging     : exception during call chain
master-localstack-1  | 2023-05-11T15:49:34.696181755Z Traceback (most recent call last):
master-localstack-1  | 2023-05-11T15:49:34.696185630Z   File "/opt/code/localstack/localstack/aws/chain.py", line 90, in handle
master-localstack-1  | 2023-05-11T15:49:34.696187838Z     handler(self, self.context, response)
master-localstack-1  | 2023-05-11T15:49:34.696189921Z   File "/opt/code/localstack/localstack/aws/handlers/service.py", line 123, in __call__
master-localstack-1  | 2023-05-11T15:49:34.696191921Z     handler(chain, context, response)
master-localstack-1  | 2023-05-11T15:49:34.696193838Z   File "/opt/code/localstack/localstack/aws/handlers/service.py", line 93, in __call__
master-localstack-1  | 2023-05-11T15:49:34.696195838Z     skeleton_response = self.skeleton.invoke(context)
master-localstack-1  | 2023-05-11T15:49:34.696210338Z   File "/opt/code/localstack/localstack/aws/skeleton.py", line 154, in invoke
master-localstack-1  | 2023-05-11T15:49:34.696216713Z     return self.dispatch_request(context, instance)
master-localstack-1  | 2023-05-11T15:49:34.696219088Z   File "/opt/code/localstack/localstack/aws/skeleton.py", line 166, in dispatch_request
master-localstack-1  | 2023-05-11T15:49:34.696221463Z     result = handler(context, instance) or {}
master-localstack-1  | 2023-05-11T15:49:34.696223380Z   File "/opt/code/localstack/localstack/aws/forwarder.py", line 60, in _call
master-localstack-1  | 2023-05-11T15:49:34.696225505Z     return handler(context, req)
master-localstack-1  | 2023-05-11T15:49:34.696227380Z   File "/opt/code/localstack/localstack/aws/skeleton.py", line 118, in __call__
master-localstack-1  | 2023-05-11T15:49:34.696229838Z     return self.fn(*args, **kwargs)
master-localstack-1  | 2023-05-11T15:49:34.696231921Z   File "/opt/code/localstack/localstack/services/sns/provider.py", line 655, in publish
master-localstack-1  | 2023-05-11T15:49:34.696234005Z     self._publisher.publish_to_topic(publish_ctx, topic_or_target_arn)
master-localstack-1  | 2023-05-11T15:49:34.696235755Z   File "/opt/code/localstack/localstack/services/sns/publisher.py", line 1141, in publish_to_topic
master-localstack-1  | 2023-05-11T15:49:34.696237921Z     if self._should_publish(ctx.store.subscription_filter_policy, ctx.message, subscriber):
master-localstack-1  | 2023-05-11T15:49:34.696240005Z   File "/opt/code/localstack/localstack/services/sns/publisher.py", line 1129, in _should_publish
master-localstack-1  | 2023-05-11T15:49:34.696242088Z     return self.subscription_filter.check_filter_policy_on_message_attributes(
master-localstack-1  | 2023-05-11T15:49:34.696244005Z   File "/opt/code/localstack/localstack/services/sns/publisher.py", line 922, in check_filter_policy_on_message_attributes
master-localstack-1  | 2023-05-11T15:49:34.696246046Z     if not self._evaluate_filter_policy_conditions_on_attribute(
master-localstack-1  | 2023-05-11T15:49:34.696247796Z   File "/opt/code/localstack/localstack/services/sns/publisher.py", line 1005, in _evaluate_filter_policy_conditions_on_attribute
master-localstack-1  | 2023-05-11T15:49:34.696249838Z     values = ast.literal_eval(val)
master-localstack-1  | 2023-05-11T15:49:34.696251671Z   File "/usr/local/lib/python3.10/ast.py", line 110, in literal_eval
master-localstack-1  | 2023-05-11T15:49:34.696253630Z     return _convert(node_or_string)
master-localstack-1  | 2023-05-11T15:49:34.696256421Z   File "/usr/local/lib/python3.10/ast.py", line 90, in _convert
master-localstack-1  | 2023-05-11T15:49:34.696258671Z     return list(map(_convert, node.elts))
master-localstack-1  | 2023-05-11T15:49:34.696277505Z   File "/usr/local/lib/python3.10/ast.py", line 109, in _convert
master-localstack-1  | 2023-05-11T15:49:34.696280255Z     return _convert_signed_num(node)
master-localstack-1  | 2023-05-11T15:49:34.696282130Z   File "/usr/local/lib/python3.10/ast.py", line 83, in _convert_signed_num
master-localstack-1  | 2023-05-11T15:49:34.696284088Z     return _convert_num(node)
master-localstack-1  | 2023-05-11T15:49:34.696285963Z   File "/usr/local/lib/python3.10/ast.py", line 74, in _convert_num
master-localstack-1  | 2023-05-11T15:49:34.696287921Z     _raise_malformed_node(node)
master-localstack-1  | 2023-05-11T15:49:34.696289880Z   File "/usr/local/lib/python3.10/ast.py", line 71, in _raise_malformed_node
master-localstack-1  | 2023-05-11T15:49:34.696292130Z     raise ValueError(msg + f': {node!r}')
master-localstack-1  | 2023-05-11T15:49:34.696310130Z ValueError: malformed node or string on line 1: <ast.Name object at 0xffff48786b90>
```


### Environment

```markdown
- LocalStack: 2.0.1 from dockerhub
```


### Anything else?

_No response_


Discussion:
Welcome to LocalStack! Thanks for reporting your first issue and our team will be working towards fixing the issue for you or reach out for more background information. We recommend joining our [Slack Community](https://localstack.cloud/contact/) for real-time help and drop a message to LocalStack Pro Support if you are a Pro user! If you are willing to contribute towards fixing this issue, please have a look at our [contributing guidelines](https://github.com/localstack/.github/blob/main/CONTRIBUTING.md) and our [contributing guide](https://docs.localstack.cloud/contributing/).

Resolving patch:
diff --git a/localstack/services/sns/publisher.py b/localstack/services/sns/publisher.py
--- a/localstack/services/sns/publisher.py
+++ b/localstack/services/sns/publisher.py
@@ -1,5 +1,4 @@
 import abc
-import ast
 import base64
 import copy
 import datetime
@@ -962,7 +961,10 @@ def _evaluate_filter_policy_conditions_on_attribute(
         tpe = attribute.get("DataType") or attribute.get("Type") if attribute else None
         val = attribute.get("StringValue") or attribute.get("Value") if attribute else None
         if attribute is not None and tpe == "String.Array":
-            values = ast.literal_eval(val)
+            try:
+                values = json.loads(val)
+            except ValueError:
+                return False
             for value in values:
                 for condition in conditions:
                     if self._evaluate_condition(value, condition, field_exists):


Test patch:
diff --git a/tests/unit/test_sns.py b/tests/unit/test_sns.py
--- a/tests/unit/test_sns.py
+++ b/tests/unit/test_sns.py
@@ -196,7 +196,7 @@ def test_filter_policy(self):
                 {
                     "filter": {
                         "Type": "String.Array",
-                        "Value": "['soccer', 'rugby', 'hockey']",
+                        "Value": '["soccer", "rugby", "hockey"]',
                     }
                 },
                 True,
@@ -226,7 +226,7 @@ def test_filter_policy(self):
                 {
                     "filter": {
                         "Type": "String.Array",
-                        "Value": "['soccer', 'rugby', 'hockey']",
+                        "Value": '["soccer", "rugby", "hockey"]',
                     }
                 },
                 True,
@@ -249,7 +249,7 @@ def test_filter_policy(self):
                 {
                     "filter": {
                         "Type": "String.Array",
-                        "Value": "['soccer', 'rugby', 'hockey']",
+                        "Value": '["soccer", "rugby", "hockey"]',
                     }
                 },
                 False,
@@ -278,7 +278,7 @@ def test_filter_policy(self):
                 {
                     "filter": {
                         "Type": "String.Array",
-                        "Value": "['soccer', 'rugby', 'hockey']",
+                        "Value": '["soccer", "rugby", "hockey"]',
                     }
                 },
                 True,
@@ -370,7 +370,7 @@ def test_filter_policy(self):
             (
                 "logical OR with match on an array",
                 {"filter": ["test1", "test2", {"prefix": "typ"}]},
-                {"filter": {"Type": "String.Array", "Value": "['test1', 'other']"}},
+                {"filter": {"Type": "String.Array", "Value": '["test1", "other"]'}},
                 True,
             ),
             (
@@ -385,7 +385,7 @@ def test_filter_policy(self):
                 {
                     "filter": {
                         "Type": "String.Array",
-                        "Value": "['anything', 'something']",
+                        "Value": '["anything", "something"]',
                     }
                 },
                 False,
@@ -474,7 +474,7 @@ def test_filter_policy(self):
                 {
                     "filter": {
                         "Type": "String.Array",
-                        "Value": "['anything', 'something']",
+                        "Value": '["anything", "something"]',
                     }
                 },
                 True,
@@ -503,6 +503,18 @@ def test_filter_policy(self):
                 {"field": {"Type": "String", "Value": "anything"}},
                 False,
             ),
+            (
+                "can match on String.Array containing boolean",
+                {"field": [True]},
+                {"field": {"Type": "String.Array", "Value": "[true]"}},
+                True,
+            ),
+            (
+                "can not match on values that are not valid JSON strings",
+                {"field": ["anything"]},
+                {"field": {"Type": "String.Array", "Value": "['anything']"}},
+                False,
+            ),
         ]
 
         sub_filter = SubscriptionFilter()