Repository: localstack/localstack

Issue:
bug: SNS Subscription Filter Policy "exists" false does not match 
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Current Behavior

I'm encountering an issue with the "exists" property on SNS Filter Policy. It logs that it can't match the policy even though I would expect it to match.

```
2021-07-28T17:57:02:INFO:localstack.services.sns.sns_listener: SNS filter policy {'source': ['source1'], 'priority': [{'exists': False}]} 
does not match attributes {'clientId': {'Type': 'String', 'Value': 'Id123'}, 'source': {'Type': 'String', 'Value': 'source1'}, 'awsRegionName': {'Type': 'String', 'Value': 'us-east-1'}}
```





### Expected Behavior

Policy matches and message is sent to subscribers.

### How are you starting LocalStack?

With a docker-compose file

### Steps To Reproduce

#### How are you starting localstack (e.g., `bin/localstack` command, arguments, or `docker-compose.yml`)

    I run in a docker-compose script and load my CloudFormation script on init.

Using the following CloudFormation script I create the subscription. 

```
"Subscription": {
            "Type": "AWS::SNS::Subscription",
            "Properties": {
                "TopicArn": {
                    "Ref": "MyTopic"
                },
                "Endpoint": {
                    "Fn::GetAtt": [
                        "MyQueue",
                        "Arn"
                    ]
                },
                "Protocol": "sqs",
                "FilterPolicy": {
                    "source": [
                        "source1"
                    ],
                    "priority":  [{"exists": false}]
                },
                "RawMessageDelivery": true
            }
        },
```



### Environment

```markdown
- OS: Windows
- LocalStack: latest
```


### Anything else?

_No response_


Discussion:
I can confirm I've just hit this with localstack/localstack:0.12.20

Resolving patch:
diff --git a/localstack/services/sns/sns_listener.py b/localstack/services/sns/sns_listener.py
--- a/localstack/services/sns/sns_listener.py
+++ b/localstack/services/sns/sns_listener.py
@@ -873,16 +873,22 @@ def evaluate_numeric_condition(conditions, value):
 
 
 def evaluate_exists_condition(conditions, message_attributes, criteria):
-    # filtering should not match any messages if the exists is set to false,As per aws docs
-    # https://docs.aws.amazon.com/sns/latest/dg/sns-subscription-filter-policies.html
+    # support for exists: false was added in april 2021
+    # https://aws.amazon.com/about-aws/whats-new/2021/04/amazon-sns-grows-the-set-of-message-filtering-operators/
     if conditions:
-        return bool(message_attributes.get(criteria))
-    return False
+        return message_attributes.get(criteria) is not None
+    else:
+        return message_attributes.get(criteria) is None
 
 
 def evaluate_condition(value, condition, message_attributes, criteria):
     if type(condition) is not dict:
         return value == condition
+    elif condition.get("exists") is not None:
+        return evaluate_exists_condition(condition.get("exists"), message_attributes, criteria)
+    elif value is None:
+        # the remaining conditions require the value to not be None
+        return False
     elif condition.get("anything-but"):
         return value not in condition.get("anything-but")
     elif condition.get("prefix"):
@@ -890,9 +896,6 @@ def evaluate_condition(value, condition, message_attributes, criteria):
         return value.startswith(prefix)
     elif condition.get("numeric"):
         return evaluate_numeric_condition(condition.get("numeric"), value)
-    elif condition.get("exists"):
-        return evaluate_exists_condition(condition.get("exists"), message_attributes, criteria)
-
     return False
 
 
@@ -900,7 +903,7 @@ def evaluate_filter_policy_conditions(conditions, attribute, message_attributes,
     if type(conditions) is not list:
         conditions = [conditions]
 
-    if attribute["Type"] == "String.Array":
+    if attribute is not None and attribute["Type"] == "String.Array":
         values = ast.literal_eval(attribute["Value"])
         for value in values:
             for condition in conditions:
@@ -908,7 +911,8 @@ def evaluate_filter_policy_conditions(conditions, attribute, message_attributes,
                     return True
     else:
         for condition in conditions:
-            if evaluate_condition(attribute["Value"], condition, message_attributes, criteria):
+            value = attribute["Value"] if attribute is not None else None
+            if evaluate_condition(value, condition, message_attributes, criteria):
                 return True
 
     return False
@@ -921,8 +925,6 @@ def check_filter_policy(filter_policy, message_attributes):
     for criteria in filter_policy:
         conditions = filter_policy.get(criteria)
         attribute = message_attributes.get(criteria)
-        if attribute is None:
-            return False
 
         if (
             evaluate_filter_policy_conditions(conditions, attribute, message_attributes, criteria)


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
@@ -538,6 +538,30 @@ def test_filter_policy(self):
                 },
                 True,
             ),
+            (
+                "exists with existing attribute",
+                {"field": [{"exists": True}]},
+                {"field": {"Type": "String", "Value": "anything"}},
+                True,
+            ),
+            (
+                "exists without existing attribute",
+                {"field": [{"exists": True}]},
+                {"other_field": {"Type": "String", "Value": "anything"}},
+                False,
+            ),
+            (
+                "does not exists without existing attribute",
+                {"field": [{"exists": False}]},
+                {"other_field": {"Type": "String", "Value": "anything"}},
+                True,
+            ),
+            (
+                "does not exists with existing attribute",
+                {"field": [{"exists": False}]},
+                {"field": {"Type": "String", "Value": "anything"}},
+                False,
+            ),
         ]
 
         for test in test_data: