Repository: getmoto/moto

Issue:
Support negative filters in secretsmanager list_secrets
Boto3 secretsmanager now supports negative filters in list_secrets ([see here](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/secretsmanager.html#SecretsManager.Client.list_secrets)).



Discussion:

Resolving patch:
diff --git a/moto/secretsmanager/list_secrets/filters.py b/moto/secretsmanager/list_secrets/filters.py
--- a/moto/secretsmanager/list_secrets/filters.py
+++ b/moto/secretsmanager/list_secrets/filters.py
@@ -1,44 +1,44 @@
-def _matcher(pattern, str):
-    for word in pattern.split(" "):
-        if word not in str:
-            return False
-    return True
-
-
 def name(secret, names):
-    for n in names:
-        if _matcher(n, secret.name):
-            return True
-    return False
+    return _matcher(names, [secret.name])
 
 
 def description(secret, descriptions):
-    for d in descriptions:
-        if _matcher(d, secret.description):
-            return True
-    return False
+    return _matcher(descriptions, [secret.description])
 
 
 def tag_key(secret, tag_keys):
-    for k in tag_keys:
-        for tag in secret.tags:
-            if _matcher(k, tag["Key"]):
-                return True
-    return False
+    return _matcher(tag_keys, [tag["Key"] for tag in secret.tags])
 
 
 def tag_value(secret, tag_values):
-    for v in tag_values:
-        for tag in secret.tags:
-            if _matcher(v, tag["Value"]):
-                return True
-    return False
+    return _matcher(tag_values, [tag["Value"] for tag in secret.tags])
 
 
 def all(secret, values):
-    return (
-        name(secret, values)
-        or description(secret, values)
-        or tag_key(secret, values)
-        or tag_value(secret, values)
+    attributes = (
+        [secret.name, secret.description]
+        + [tag["Key"] for tag in secret.tags]
+        + [tag["Value"] for tag in secret.tags]
     )
+
+    return _matcher(values, attributes)
+
+
+def _matcher(patterns, strings):
+    for pattern in [p for p in patterns if p.startswith("!")]:
+        for string in strings:
+            if _match_pattern(pattern[1:], string):
+                return False
+
+    for pattern in [p for p in patterns if not p.startswith("!")]:
+        for string in strings:
+            if _match_pattern(pattern, string):
+                return True
+    return False
+
+
+def _match_pattern(pattern, str):
+    for word in pattern.split(" "):
+        if word not in str:
+            return False
+    return True


Test patch:
diff --git a/tests/test_secretsmanager/test_list_secrets.py b/tests/test_secretsmanager/test_list_secrets.py
--- a/tests/test_secretsmanager/test_list_secrets.py
+++ b/tests/test_secretsmanager/test_list_secrets.py
@@ -244,3 +244,21 @@ def test_with_filter_with_value_with_multiple_words():
 
     secret_names = list(map(lambda s: s["Name"], secrets["SecretList"]))
     assert secret_names == ["foo", "bar"]
+
+
+@mock_secretsmanager
+def test_with_filter_with_negation():
+    conn = boto_client()
+
+    conn.create_secret(Name="foo", SecretString="secret", Description="one two")
+    conn.create_secret(Name="bar", SecretString="secret", Description="one and two")
+    conn.create_secret(Name="baz", SecretString="secret", Description="one")
+    conn.create_secret(Name="qux", SecretString="secret", Description="two")
+    conn.create_secret(Name="none", SecretString="secret", Description="unrelated")
+
+    secrets = conn.list_secrets(
+        Filters=[{"Key": "description", "Values": ["one", "!two"]}]
+    )
+
+    secret_names = list(map(lambda s: s["Name"], secrets["SecretList"]))
+    assert secret_names == ["baz"]