Repository: cloud-custodian/cloud-custodian

Issue:
rds-snapshot action to revert non-whitelisted account
Enhancement request for an action to revert any account added via modify_db_snapshot_attribute event that is not in a whitelist.

Expectation is this new action would be used in a policy using cloudtrail event and using rds-snapshot.filters.cross-account


Discussion:

Resolving patch:
diff --git a/c7n/resources/rds.py b/c7n/resources/rds.py
--- a/c7n/resources/rds.py
+++ b/c7n/resources/rds.py
@@ -1158,6 +1158,8 @@ def get_restore_from_tags(self, snapshot):
 class CrossAccountAccess(CrossAccountAccessFilter):
 
     permissions = ('rds:DescribeDBSnapshotAttributes',)
+    attributes_key = 'c7n:attributes'
+    annotation_key = 'c7n:CrossAccountViolations'
 
     def process(self, resources, event=None):
         self.accounts = self.get_accounts()
@@ -1185,15 +1187,108 @@ def process_resource_set(self, resource_set):
                 client.describe_db_snapshot_attributes,
                 DBSnapshotIdentifier=r['DBSnapshotIdentifier'])[
                     'DBSnapshotAttributesResult']['DBSnapshotAttributes']}
-            r['c7n:attributes'] = attrs
+            r[self.attributes_key] = attrs
             shared_accounts = set(attrs.get('restore', []))
             delta_accounts = shared_accounts.difference(self.accounts)
             if delta_accounts:
-                r['c7n:CrossAccountViolations'] = list(delta_accounts)
+                r[self.annotation_key] = list(delta_accounts)
                 results.append(r)
         return results
 
 
+@RDSSnapshot.action_registry.register('set-permissions')
+class SetPermissions(BaseAction):
+    """Set permissions for copying or restoring an RDS snapshot
+
+    Use the 'add' and 'remove' parameters to control which accounts to
+    add or remove, respectively.  The default is to remove any
+    permissions granted to other AWS accounts.
+
+    Use `remove: matched` in combination with the `cross-account` filter
+    for more flexible removal options such as preserving access for
+    a set of whitelisted accounts:
+
+    :example:
+
+    .. code-block:: yaml
+
+            policies:
+              - name: rds-snapshot-remove-cross-account
+                resource: rds-snapshot
+                filters:
+                  - type: cross-account
+                    whitelist:
+                      - '112233445566'
+                actions:
+                  - type: set-permissions
+                    remove: matched
+    """
+    schema = type_schema(
+        'set-permissions',
+        remove={'oneOf': [
+            {'enum': ['matched']},
+            {'type': 'array', 'items': {
+                'oneOf': [
+                    {'type': 'string', 'minLength': 12, 'maxLength': 12},
+                    {'enum': ['all']},
+                ],
+            }}
+        ]},
+        add={
+            'type': 'array', 'items': {
+                'oneOf': [
+                    {'type': 'string', 'minLength': 12, 'maxLength': 12},
+                    {'enum': ['all']},
+                ]
+            }
+        }
+    )
+
+    permissions = ('rds:ModifyDBSnapshotAttribute',)
+
+    def validate(self):
+        if self.data.get('remove') == 'matched':
+            found = False
+            for f in self.manager.iter_filters():
+                if isinstance(f, CrossAccountAccessFilter):
+                    found = True
+                    break
+            if not found:
+                raise PolicyValidationError(
+                    "policy:%s filter:%s with matched requires cross-account filter" % (
+                        self.manager.ctx.policy.name, self.type))
+
+    def process(self, snapshots):
+        client = local_session(self.manager.session_factory).client('rds')
+        for s in snapshots:
+            self.process_snapshot(client, s)
+
+    def process_snapshot(self, client, snapshot):
+        add_accounts = self.data.get('add', [])
+        remove_accounts = self.data.get('remove', [])
+
+        if not (add_accounts or remove_accounts):
+            if CrossAccountAccess.attributes_key not in snapshot:
+                attrs = {
+                    t['AttributeName']: t['AttributeValues']
+                    for t in self.manager.retry(
+                        client.describe_db_snapshot_attributes,
+                        DBSnapshotIdentifier=snapshot['DBSnapshotIdentifier']
+                    )['DBSnapshotAttributesResult']['DBSnapshotAttributes']
+                }
+                snapshot[CrossAccountAccess.attributes_key] = attrs
+            remove_accounts = snapshot[CrossAccountAccess.attributes_key].get('restore', [])
+        elif remove_accounts == 'matched':
+            remove_accounts = snapshot.get(CrossAccountAccess.annotation_key, [])
+
+        if add_accounts or remove_accounts:
+            client.modify_db_snapshot_attribute(
+                DBSnapshotIdentifier=snapshot['DBSnapshotIdentifier'],
+                AttributeName='restore',
+                ValuesToRemove=remove_accounts,
+                ValuesToAdd=add_accounts)
+
+
 @RDSSnapshot.action_registry.register('region-copy')
 class RegionCopySnapshot(BaseAction):
     """Copy a snapshot across regions.
diff --git a/c7n/resources/rdscluster.py b/c7n/resources/rdscluster.py
--- a/c7n/resources/rdscluster.py
+++ b/c7n/resources/rdscluster.py
@@ -10,6 +10,7 @@
 import c7n.filters.vpc as net_filters
 from c7n.manager import resources
 from c7n.query import ConfigSource, QueryResourceManager, TypeInfo, DescribeSource
+from c7n.resources import rds
 from .aws import shape_validate
 from c7n.exceptions import PolicyValidationError
 from c7n.utils import (
@@ -389,7 +390,6 @@ def load_resource(self, item):
                 resource.pop(k)
                 k = 'IAMD%s' % k[4:]
                 resource[k] = v
-        resource['Tags'] = [{'Key': k, 'Value': v} for k, v in item['tags'].items()]
         return resource
 
 
@@ -421,6 +421,8 @@ class resource_type(TypeInfo):
 class CrossAccountSnapshot(CrossAccountAccessFilter):
 
     permissions = ('rds:DescribeDBClusterSnapshotAttributes',)
+    attributes_key = 'c7n:attributes'
+    annotation_key = 'c7n:CrossAccountViolations'
 
     def process(self, resources, event=None):
         self.accounts = self.get_accounts()
@@ -443,11 +445,11 @@ def process_resource_set(self, resource_set):
                 client.describe_db_cluster_snapshot_attributes,
                      DBClusterSnapshotIdentifier=r['DBClusterSnapshotIdentifier'])[
                          'DBClusterSnapshotAttributesResult']['DBClusterSnapshotAttributes']}
-            r['c7n:attributes'] = attrs
+            r[self.attributes_key] = attrs
             shared_accounts = set(attrs.get('restore', []))
             delta_accounts = shared_accounts.difference(self.accounts)
             if delta_accounts:
-                r['c7n:CrossAccountViolations'] = list(delta_accounts)
+                r[self.annotation_key] = list(delta_accounts)
                 results.append(r)
         return results
 
@@ -476,6 +478,61 @@ class RDSSnapshotAge(AgeFilter):
     date_attribute = 'SnapshotCreateTime'
 
 
+@RDSClusterSnapshot.action_registry.register('set-permissions')
+class SetPermissions(rds.SetPermissions):
+    """Set permissions for copying or restoring an RDS cluster snapshot
+
+    Use the 'add' and 'remove' parameters to control which accounts to
+    add or remove, respectively.  The default is to remove any
+    permissions granted to other AWS accounts.
+
+    Use `remove: matched` in combination with the `cross-account` filter
+    for more flexible removal options such as preserving access for
+    a set of whitelisted accounts:
+
+    :example:
+
+    .. code-block:: yaml
+
+            policies:
+              - name: rds-cluster-snapshot-prune-permissions
+                resource: rds-cluster-snapshot
+                filters:
+                  - type: cross-account
+                    whitelist:
+                      - '112233445566'
+                actions:
+                  - type: set-permissions
+                    remove: matched
+    """
+    permissions = ('rds:ModifyDBClusterSnapshotAttribute',)
+
+    def process_snapshot(self, client, snapshot):
+        add_accounts = self.data.get('add', [])
+        remove_accounts = self.data.get('remove', [])
+
+        if not (add_accounts or remove_accounts):
+            if CrossAccountSnapshot.attributes_key not in snapshot:
+                attrs = {
+                    t['AttributeName']: t['AttributeValues']
+                    for t in self.manager.retry(
+                        client.describe_db_cluster_snapshot_attributes,
+                        DBClusterSnapshotIdentifier=snapshot['DBClusterSnapshotIdentifier']
+                    )['DBClusterSnapshotAttributesResult']['DBClusterSnapshotAttributes']
+                }
+                snapshot[CrossAccountSnapshot.attributes_key] = attrs
+            remove_accounts = snapshot[CrossAccountSnapshot.attributes_key].get('restore', [])
+        elif remove_accounts == 'matched':
+            remove_accounts = snapshot.get(CrossAccountSnapshot.annotation_key, [])
+
+        if add_accounts or remove_accounts:
+            client.modify_db_cluster_snapshot_attribute(
+                DBClusterSnapshotIdentifier=snapshot['DBClusterSnapshotIdentifier'],
+                AttributeName='restore',
+                ValuesToRemove=remove_accounts,
+                ValuesToAdd=add_accounts)
+
+
 @RDSClusterSnapshot.action_registry.register('delete')
 class RDSClusterSnapshotDelete(BaseAction):
     """Action to delete rds cluster snapshots


Test patch:
diff --git a/tests/data/placebo/test_rds_snapshot_clear_permissions/config.SelectResourceConfig_1.json b/tests/data/placebo/test_rds_snapshot_clear_permissions/config.SelectResourceConfig_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_rds_snapshot_clear_permissions/config.SelectResourceConfig_1.json
@@ -0,0 +1,22 @@
+{
+    "status_code": 200,
+    "data": {
+        "Results": [
+            "{\"resourceId\":\"testing\",\"configuration\":{\"allocatedStorage\":5.0,\"availabilityZone\":\"us-east-2b\",\"dbiResourceId\":\"db-YOIV3STYJIHNL2ASADGMMJI3TA\",\"dBInstanceIdentifier\":\"demodb\",\"engine\":\"mysql\",\"vpcId\":\"vpc-00e3a582e54e7f09a\",\"percentProgress\":100.0,\"iAMDatabaseAuthenticationEnabled\":false,\"dBSnapshotIdentifier\":\"testing\",\"optionGroupName\":\"demodb-20201216173045036600000003\",\"engineVersion\":\"5.7.19\",\"masterUsername\":\"user\",\"snapshotType\":\"manual\",\"instanceCreateTime\":\"2020-12-16T17:34:24.037Z\",\"dBSnapshotArn\":\"arn:aws:rds:us-east-2:644160558196:snapshot:testing\",\"processorFeatures\":[],\"tagList\":[],\"encrypted\":false,\"port\":3306.0,\"snapshotCreateTime\":\"2020-12-30T05:34:38.901Z\",\"licenseModel\":\"general-public-license\",\"storageType\":\"gp2\",\"status\":\"available\"},\"supplementaryConfiguration\":{\"DBSnapshotAttributes\":[{\"attributeValues\":[],\"attributeName\":\"restore\"}],\"Tags\":[]}}"
+        ],
+        "QueryInfo": {
+            "SelectFields": [
+                {
+                    "Name": "resourceId"
+                },
+                {
+                    "Name": "configuration"
+                },
+                {
+                    "Name": "supplementaryConfiguration"
+                }
+            ]
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_rds_snapshot_clear_permissions/rds.DescribeDBSnapshotAttributes_1.json b/tests/data/placebo/test_rds_snapshot_clear_permissions/rds.DescribeDBSnapshotAttributes_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_rds_snapshot_clear_permissions/rds.DescribeDBSnapshotAttributes_1.json
@@ -0,0 +1,19 @@
+{
+    "status_code": 200,
+    "data": {
+        "DBSnapshotAttributesResult": {
+            "DBSnapshotIdentifier": "testing",
+            "DBSnapshotAttributes": [
+                {
+                    "AttributeName": "restore",
+                    "AttributeValues": [
+                        "all",
+                        "644160558196",
+                        "123456789012"
+                    ]
+                }
+            ]
+        },
+        "ResponseMetadata": {}
+    }
+}
diff --git a/tests/data/placebo/test_rds_snapshot_clear_permissions/rds.DescribeDBSnapshotAttributes_2.json b/tests/data/placebo/test_rds_snapshot_clear_permissions/rds.DescribeDBSnapshotAttributes_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_rds_snapshot_clear_permissions/rds.DescribeDBSnapshotAttributes_2.json
@@ -0,0 +1,15 @@
+{
+    "status_code": 200,
+    "data": {
+        "DBSnapshotAttributesResult": {
+            "DBSnapshotIdentifier": "testing",
+            "DBSnapshotAttributes": [
+                {
+                    "AttributeName": "restore",
+                    "AttributeValues": []
+                }
+            ]
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_rds_snapshot_clear_permissions/rds.ModifyDBSnapshotAttribute_1.json b/tests/data/placebo/test_rds_snapshot_clear_permissions/rds.ModifyDBSnapshotAttribute_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_rds_snapshot_clear_permissions/rds.ModifyDBSnapshotAttribute_1.json
@@ -0,0 +1,15 @@
+{
+    "status_code": 200,
+    "data": {
+        "DBSnapshotAttributesResult": {
+            "DBSnapshotIdentifier": "testing",
+            "DBSnapshotAttributes": [
+                {
+                    "AttributeName": "restore",
+                    "AttributeValues": []
+                }
+            ]
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_rds_snapshot_remove_matched_permissions/config.SelectResourceConfig_1.json b/tests/data/placebo/test_rds_snapshot_remove_matched_permissions/config.SelectResourceConfig_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_rds_snapshot_remove_matched_permissions/config.SelectResourceConfig_1.json
@@ -0,0 +1,22 @@
+{
+    "status_code": 200,
+    "data": {
+        "Results": [
+            "{\"resourceId\":\"testing\",\"configuration\":{\"allocatedStorage\":5.0,\"availabilityZone\":\"us-east-2b\",\"dbiResourceId\":\"db-YOIV3STYJIHNL2ASADGMMJI3TA\",\"dBInstanceIdentifier\":\"demodb\",\"engine\":\"mysql\",\"vpcId\":\"vpc-00e3a582e54e7f09a\",\"percentProgress\":100.0,\"iAMDatabaseAuthenticationEnabled\":false,\"dBSnapshotIdentifier\":\"testing\",\"optionGroupName\":\"demodb-20201216173045036600000003\",\"engineVersion\":\"5.7.19\",\"masterUsername\":\"user\",\"snapshotType\":\"manual\",\"instanceCreateTime\":\"2020-12-16T17:34:24.037Z\",\"dBSnapshotArn\":\"arn:aws:rds:us-east-2:644160558196:snapshot:testing\",\"processorFeatures\":[],\"tagList\":[],\"encrypted\":false,\"port\":3306.0,\"snapshotCreateTime\":\"2020-12-30T05:34:38.901Z\",\"licenseModel\":\"general-public-license\",\"storageType\":\"gp2\",\"status\":\"available\"},\"supplementaryConfiguration\":{\"DBSnapshotAttributes\":[{\"attributeValues\":[],\"attributeName\":\"restore\"}],\"Tags\":[]}}"
+        ],
+        "QueryInfo": {
+            "SelectFields": [
+                {
+                    "Name": "resourceId"
+                },
+                {
+                    "Name": "configuration"
+                },
+                {
+                    "Name": "supplementaryConfiguration"
+                }
+            ]
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_rds_snapshot_remove_matched_permissions/rds.DescribeDBSnapshotAttributes_1.json b/tests/data/placebo/test_rds_snapshot_remove_matched_permissions/rds.DescribeDBSnapshotAttributes_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_rds_snapshot_remove_matched_permissions/rds.DescribeDBSnapshotAttributes_1.json
@@ -0,0 +1,19 @@
+{
+    "status_code": 200,
+    "data": {
+        "DBSnapshotAttributesResult": {
+            "DBSnapshotIdentifier": "testing",
+            "DBSnapshotAttributes": [
+                {
+                    "AttributeName": "restore",
+                    "AttributeValues": [
+                        "all",
+                        "644160558196",
+                        "123456789012"
+                    ]
+                }
+            ]
+        },
+        "ResponseMetadata": {}
+    }
+}
diff --git a/tests/data/placebo/test_rds_snapshot_remove_matched_permissions/rds.DescribeDBSnapshotAttributes_2.json b/tests/data/placebo/test_rds_snapshot_remove_matched_permissions/rds.DescribeDBSnapshotAttributes_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_rds_snapshot_remove_matched_permissions/rds.DescribeDBSnapshotAttributes_2.json
@@ -0,0 +1,17 @@
+{
+    "status_code": 200,
+    "data": {
+        "DBSnapshotAttributesResult": {
+            "DBSnapshotIdentifier": "testing",
+            "DBSnapshotAttributes": [
+                {
+                    "AttributeName": "restore",
+                    "AttributeValues": [
+                        "644160558196"
+                    ]
+                }
+            ]
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_rds_snapshot_remove_matched_permissions/rds.ModifyDBSnapshotAttribute_1.json b/tests/data/placebo/test_rds_snapshot_remove_matched_permissions/rds.ModifyDBSnapshotAttribute_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_rds_snapshot_remove_matched_permissions/rds.ModifyDBSnapshotAttribute_1.json
@@ -0,0 +1,17 @@
+{
+    "status_code": 200,
+    "data": {
+        "DBSnapshotAttributesResult": {
+            "DBSnapshotIdentifier": "testing",
+            "DBSnapshotAttributes": [
+                {
+                    "AttributeName": "restore",
+                    "AttributeValues": [
+                        "644160558196"
+                    ]
+                }
+            ]
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_rds_snapshot_set_permissions/config.SelectResourceConfig_1.json b/tests/data/placebo/test_rds_snapshot_set_permissions/config.SelectResourceConfig_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_rds_snapshot_set_permissions/config.SelectResourceConfig_1.json
@@ -0,0 +1,22 @@
+{
+    "status_code": 200,
+    "data": {
+        "Results": [
+            "{\"resourceId\":\"testing\",\"configuration\":{\"allocatedStorage\":5.0,\"availabilityZone\":\"us-east-2b\",\"dbiResourceId\":\"db-YOIV3STYJIHNL2ASADGMMJI3TA\",\"dBInstanceIdentifier\":\"demodb\",\"engine\":\"mysql\",\"vpcId\":\"vpc-00e3a582e54e7f09a\",\"percentProgress\":100.0,\"iAMDatabaseAuthenticationEnabled\":false,\"dBSnapshotIdentifier\":\"testing\",\"optionGroupName\":\"demodb-20201216173045036600000003\",\"engineVersion\":\"5.7.19\",\"masterUsername\":\"user\",\"snapshotType\":\"manual\",\"instanceCreateTime\":\"2020-12-16T17:34:24.037Z\",\"dBSnapshotArn\":\"arn:aws:rds:us-east-2:644160558196:snapshot:testing\",\"processorFeatures\":[],\"tagList\":[],\"encrypted\":false,\"port\":3306.0,\"snapshotCreateTime\":\"2020-12-30T05:34:38.901Z\",\"licenseModel\":\"general-public-license\",\"storageType\":\"gp2\",\"status\":\"available\"},\"supplementaryConfiguration\":{\"DBSnapshotAttributes\":[{\"attributeValues\":[\"644160558196\",\"644160558196\",\"644160558196\",\"644160558196\"],\"attributeName\":\"restore\"}],\"Tags\":[]}}"
+        ],
+        "QueryInfo": {
+            "SelectFields": [
+                {
+                    "Name": "resourceId"
+                },
+                {
+                    "Name": "configuration"
+                },
+                {
+                    "Name": "supplementaryConfiguration"
+                }
+            ]
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_rds_snapshot_set_permissions/rds.DescribeDBSnapshotAttributes_1.json b/tests/data/placebo/test_rds_snapshot_set_permissions/rds.DescribeDBSnapshotAttributes_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_rds_snapshot_set_permissions/rds.DescribeDBSnapshotAttributes_1.json
@@ -0,0 +1,19 @@
+{
+    "status_code": 200,
+    "data": {
+        "DBSnapshotAttributesResult": {
+            "DBSnapshotIdentifier": "testing",
+            "DBSnapshotAttributes": [
+                {
+                    "AttributeName": "restore",
+                    "AttributeValues": [
+                        "all",
+                        "644160558196",
+                        "123456789012"
+                    ]
+                }
+            ]
+        },
+        "ResponseMetadata": {}
+    }
+}
diff --git a/tests/data/placebo/test_rds_snapshot_set_permissions/rds.DescribeDBSnapshotAttributes_2.json b/tests/data/placebo/test_rds_snapshot_set_permissions/rds.DescribeDBSnapshotAttributes_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_rds_snapshot_set_permissions/rds.DescribeDBSnapshotAttributes_2.json
@@ -0,0 +1,18 @@
+{
+    "status_code": 200,
+    "data": {
+        "DBSnapshotAttributesResult": {
+            "DBSnapshotIdentifier": "testing",
+            "DBSnapshotAttributes": [
+                {
+                    "AttributeName": "restore",
+                    "AttributeValues": [
+                        "644160558196",
+                        "234567890123"
+                    ]
+                }
+            ]
+        },
+        "ResponseMetadata": {}
+    }
+}
diff --git a/tests/data/placebo/test_rds_snapshot_set_permissions/rds.ModifyDBSnapshotAttribute_1.json b/tests/data/placebo/test_rds_snapshot_set_permissions/rds.ModifyDBSnapshotAttribute_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_rds_snapshot_set_permissions/rds.ModifyDBSnapshotAttribute_1.json
@@ -0,0 +1,18 @@
+{
+    "status_code": 200,
+    "data": {
+        "DBSnapshotAttributesResult": {
+            "DBSnapshotIdentifier": "testing",
+            "DBSnapshotAttributes": [
+                {
+                    "AttributeName": "restore",
+                    "AttributeValues": [
+                        "644160558196",
+                        "644160558196"
+                    ]
+                }
+            ]
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_rdscluster_snapshot_clear_permissions/config.SelectResourceConfig_1.json b/tests/data/placebo/test_rdscluster_snapshot_clear_permissions/config.SelectResourceConfig_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_rdscluster_snapshot_clear_permissions/config.SelectResourceConfig_1.json
@@ -0,0 +1,22 @@
+{
+    "status_code": 200,
+    "data": {
+        "Results": [
+            "{\"resourceId\":\"test\",\"configuration\":{\"engineVersion\":\"5.7.mysql_aurora.2.03.2\",\"masterUsername\":\"testing\",\"allocatedStorage\":0.0,\"snapshotType\":\"manual\",\"availabilityZones\":[{\"value\":\"us-east-2a\"},{\"value\":\"us-east-2b\"},{\"value\":\"us-east-2c\"}],\"storageEncrypted\":false,\"dbclusterSnapshotIdentifier\":\"test\",\"tagList\":[{\"value\":\"dev\",\"key\":\"Environment\"}],\"iamdatabaseAuthenticationEnabled\":false,\"dbclusterSnapshotArn\":\"arn:aws:rds:us-east-2:644160558196:cluster-snapshot:test\",\"dbclusterIdentifier\":\"test-cluster\",\"engine\":\"aurora-mysql\",\"port\":0.0,\"snapshotCreateTime\":1.609308615222E12,\"licenseModel\":\"aurora-mysql\",\"vpcId\":\"vpc-00e3a582e54e7f09a\",\"percentProgress\":100.0,\"clusterCreateTime\":1.609306416945E12,\"status\":\"available\"},\"supplementaryConfiguration\":{\"DBClusterSnapshotAttributes\":[{\"attributeValues\":[\"all\",\"644160558196\",\"644160558196\"],\"attributeName\":\"restore\"}],\"Tags\":[{\"value\":\"dev\",\"key\":\"Environment\"}]}}"
+        ],
+        "QueryInfo": {
+            "SelectFields": [
+                {
+                    "Name": "resourceId"
+                },
+                {
+                    "Name": "configuration"
+                },
+                {
+                    "Name": "supplementaryConfiguration"
+                }
+            ]
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_rdscluster_snapshot_clear_permissions/rds.DescribeDBClusterSnapshotAttributes_1.json b/tests/data/placebo/test_rdscluster_snapshot_clear_permissions/rds.DescribeDBClusterSnapshotAttributes_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_rdscluster_snapshot_clear_permissions/rds.DescribeDBClusterSnapshotAttributes_1.json
@@ -0,0 +1,19 @@
+{
+    "status_code": 200,
+    "data": {
+        "DBClusterSnapshotAttributesResult": {
+            "DBClusterSnapshotIdentifier": "test",
+            "DBClusterSnapshotAttributes": [
+                {
+                    "AttributeName": "restore",
+                    "AttributeValues": [
+                        "all",
+                        "644160558196",
+                        "123456789012"
+                    ]
+                }
+            ]
+        },
+        "ResponseMetadata": {}
+    }
+}
diff --git a/tests/data/placebo/test_rdscluster_snapshot_clear_permissions/rds.DescribeDBClusterSnapshotAttributes_2.json b/tests/data/placebo/test_rdscluster_snapshot_clear_permissions/rds.DescribeDBClusterSnapshotAttributes_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_rdscluster_snapshot_clear_permissions/rds.DescribeDBClusterSnapshotAttributes_2.json
@@ -0,0 +1,15 @@
+{
+    "status_code": 200,
+    "data": {
+        "DBClusterSnapshotAttributesResult": {
+            "DBClusterSnapshotIdentifier": "test",
+            "DBClusterSnapshotAttributes": [
+                {
+                    "AttributeName": "restore",
+                    "AttributeValues": []
+                }
+            ]
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_rdscluster_snapshot_clear_permissions/rds.ModifyDBClusterSnapshotAttribute_1.json b/tests/data/placebo/test_rdscluster_snapshot_clear_permissions/rds.ModifyDBClusterSnapshotAttribute_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_rdscluster_snapshot_clear_permissions/rds.ModifyDBClusterSnapshotAttribute_1.json
@@ -0,0 +1,15 @@
+{
+    "status_code": 200,
+    "data": {
+        "DBClusterSnapshotAttributesResult": {
+            "DBClusterSnapshotIdentifier": "test",
+            "DBClusterSnapshotAttributes": [
+                {
+                    "AttributeName": "restore",
+                    "AttributeValues": []
+                }
+            ]
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_rdscluster_snapshot_remove_matched_permissions/config.SelectResourceConfig_1.json b/tests/data/placebo/test_rdscluster_snapshot_remove_matched_permissions/config.SelectResourceConfig_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_rdscluster_snapshot_remove_matched_permissions/config.SelectResourceConfig_1.json
@@ -0,0 +1,22 @@
+{
+    "status_code": 200,
+    "data": {
+        "Results": [
+            "{\"resourceId\":\"test\",\"configuration\":{\"engineVersion\":\"5.7.mysql_aurora.2.03.2\",\"masterUsername\":\"testing\",\"allocatedStorage\":0.0,\"snapshotType\":\"manual\",\"availabilityZones\":[{\"value\":\"us-east-2a\"},{\"value\":\"us-east-2b\"},{\"value\":\"us-east-2c\"}],\"storageEncrypted\":false,\"dbclusterSnapshotIdentifier\":\"test\",\"tagList\":[],\"iamdatabaseAuthenticationEnabled\":false,\"dbclusterIdentifier\":\"test-cluster\",\"dbclusterSnapshotArn\":\"arn:aws:rds:us-east-2:644160558196:cluster-snapshot:test\",\"engine\":\"aurora-mysql\",\"port\":0.0,\"snapshotCreateTime\":1.609308615222E12,\"licenseModel\":\"aurora-mysql\",\"vpcId\":\"vpc-00e3a582e54e7f09a\",\"percentProgress\":100.0,\"clusterCreateTime\":1.609306416945E12,\"status\":\"available\"},\"supplementaryConfiguration\":{\"DBClusterSnapshotAttributes\":[{\"attributeValues\":[\"all\",\"644160558196\",\"644160558196\"],\"attributeName\":\"restore\"}],\"Tags\":[]}}"
+        ],
+        "QueryInfo": {
+            "SelectFields": [
+                {
+                    "Name": "resourceId"
+                },
+                {
+                    "Name": "configuration"
+                },
+                {
+                    "Name": "supplementaryConfiguration"
+                }
+            ]
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_rdscluster_snapshot_remove_matched_permissions/rds.DescribeDBClusterSnapshotAttributes_1.json b/tests/data/placebo/test_rdscluster_snapshot_remove_matched_permissions/rds.DescribeDBClusterSnapshotAttributes_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_rdscluster_snapshot_remove_matched_permissions/rds.DescribeDBClusterSnapshotAttributes_1.json
@@ -0,0 +1,19 @@
+{
+    "status_code": 200,
+    "data": {
+        "DBClusterSnapshotAttributesResult": {
+            "DBClusterSnapshotIdentifier": "test",
+            "DBClusterSnapshotAttributes": [
+                {
+                    "AttributeName": "restore",
+                    "AttributeValues": [
+                        "all",
+                        "644160558196",
+                        "123456789012"
+                    ]
+                }
+            ]
+        },
+        "ResponseMetadata": {}
+    }
+}
diff --git a/tests/data/placebo/test_rdscluster_snapshot_remove_matched_permissions/rds.DescribeDBClusterSnapshotAttributes_2.json b/tests/data/placebo/test_rdscluster_snapshot_remove_matched_permissions/rds.DescribeDBClusterSnapshotAttributes_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_rdscluster_snapshot_remove_matched_permissions/rds.DescribeDBClusterSnapshotAttributes_2.json
@@ -0,0 +1,17 @@
+{
+    "status_code": 200,
+    "data": {
+        "DBClusterSnapshotAttributesResult": {
+            "DBClusterSnapshotIdentifier": "test",
+            "DBClusterSnapshotAttributes": [
+                {
+                    "AttributeName": "restore",
+                    "AttributeValues": [
+                        "644160558196"
+                    ]
+                }
+            ]
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_rdscluster_snapshot_remove_matched_permissions/rds.ModifyDBClusterSnapshotAttribute_1.json b/tests/data/placebo/test_rdscluster_snapshot_remove_matched_permissions/rds.ModifyDBClusterSnapshotAttribute_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_rdscluster_snapshot_remove_matched_permissions/rds.ModifyDBClusterSnapshotAttribute_1.json
@@ -0,0 +1,17 @@
+{
+    "status_code": 200,
+    "data": {
+        "DBClusterSnapshotAttributesResult": {
+            "DBClusterSnapshotIdentifier": "test",
+            "DBClusterSnapshotAttributes": [
+                {
+                    "AttributeName": "restore",
+                    "AttributeValues": [
+                        "644160558196"
+                    ]
+                }
+            ]
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_rdscluster_snapshot_set_permissions/config.SelectResourceConfig_1.json b/tests/data/placebo/test_rdscluster_snapshot_set_permissions/config.SelectResourceConfig_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_rdscluster_snapshot_set_permissions/config.SelectResourceConfig_1.json
@@ -0,0 +1,22 @@
+{
+    "status_code": 200,
+    "data": {
+        "Results": [
+            "{\"resourceId\":\"test\",\"configuration\":{\"engineVersion\":\"5.7.mysql_aurora.2.03.2\",\"masterUsername\":\"testing\",\"allocatedStorage\":0.0,\"snapshotType\":\"manual\",\"availabilityZones\":[{\"value\":\"us-east-2a\"},{\"value\":\"us-east-2b\"},{\"value\":\"us-east-2c\"}],\"storageEncrypted\":false,\"dbclusterSnapshotIdentifier\":\"test\",\"tagList\":[],\"iamdatabaseAuthenticationEnabled\":false,\"dbclusterIdentifier\":\"test-cluster\",\"dbclusterSnapshotArn\":\"arn:aws:rds:us-east-2:644160558196:cluster-snapshot:test\",\"engine\":\"aurora-mysql\",\"port\":0.0,\"snapshotCreateTime\":1.609308615222E12,\"licenseModel\":\"aurora-mysql\",\"vpcId\":\"vpc-00e3a582e54e7f09a\",\"percentProgress\":100.0,\"clusterCreateTime\":1.609306416945E12,\"status\":\"available\"},\"supplementaryConfiguration\":{\"DBClusterSnapshotAttributes\":[{\"attributeValues\":[\"all\",\"644160558196\",\"644160558196\"],\"attributeName\":\"restore\"}],\"Tags\":[]}}"
+        ],
+        "QueryInfo": {
+            "SelectFields": [
+                {
+                    "Name": "resourceId"
+                },
+                {
+                    "Name": "configuration"
+                },
+                {
+                    "Name": "supplementaryConfiguration"
+                }
+            ]
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_rdscluster_snapshot_set_permissions/rds.DescribeDBClusterSnapshotAttributes_1.json b/tests/data/placebo/test_rdscluster_snapshot_set_permissions/rds.DescribeDBClusterSnapshotAttributes_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_rdscluster_snapshot_set_permissions/rds.DescribeDBClusterSnapshotAttributes_1.json
@@ -0,0 +1,19 @@
+{
+    "status_code": 200,
+    "data": {
+        "DBClusterSnapshotAttributesResult": {
+            "DBClusterSnapshotIdentifier": "test",
+            "DBClusterSnapshotAttributes": [
+                {
+                    "AttributeName": "restore",
+                    "AttributeValues": [
+                        "all",
+                        "644160558196",
+                        "123456789012"
+                    ]
+                }
+            ]
+        },
+        "ResponseMetadata": {}
+    }
+}
diff --git a/tests/data/placebo/test_rdscluster_snapshot_set_permissions/rds.DescribeDBClusterSnapshotAttributes_2.json b/tests/data/placebo/test_rdscluster_snapshot_set_permissions/rds.DescribeDBClusterSnapshotAttributes_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_rdscluster_snapshot_set_permissions/rds.DescribeDBClusterSnapshotAttributes_2.json
@@ -0,0 +1,18 @@
+{
+    "status_code": 200,
+    "data": {
+        "DBClusterSnapshotAttributesResult": {
+            "DBClusterSnapshotIdentifier": "test",
+            "DBClusterSnapshotAttributes": [
+                {
+                    "AttributeName": "restore",
+                    "AttributeValues": [
+                        "644160558196",
+                        "234567890123"
+                    ]
+                }
+            ]
+        },
+        "ResponseMetadata": {}
+    }
+}
diff --git a/tests/data/placebo/test_rdscluster_snapshot_set_permissions/rds.ModifyDBClusterSnapshotAttribute_1.json b/tests/data/placebo/test_rdscluster_snapshot_set_permissions/rds.ModifyDBClusterSnapshotAttribute_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_rdscluster_snapshot_set_permissions/rds.ModifyDBClusterSnapshotAttribute_1.json
@@ -0,0 +1,18 @@
+{
+    "status_code": 200,
+    "data": {
+        "DBClusterSnapshotAttributesResult": {
+            "DBClusterSnapshotIdentifier": "test",
+            "DBClusterSnapshotAttributes": [
+                {
+                    "AttributeName": "restore",
+                    "AttributeValues": [
+                        "644160558196",
+                        "644160558196"
+                    ]
+                }
+            ]
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/test_rds.py b/tests/test_rds.py
--- a/tests/test_rds.py
+++ b/tests/test_rds.py
@@ -1243,6 +1243,136 @@ def test_rds_public_accessible_disable(self):
             ]
         )
 
+    def _get_effective_permissions(self, client, snapshot_id):
+        attributes = client.describe_db_snapshot_attributes(
+            DBSnapshotIdentifier=snapshot_id
+        )["DBSnapshotAttributesResult"]["DBSnapshotAttributes"]
+        attr_map = {
+            attr["AttributeName"]: attr["AttributeValues"]
+            for attr in attributes
+        }
+        return set(attr_map.get("restore", []))
+
+    def test_set_permissions(self):
+        session_factory = self.replay_flight_data(
+            "test_rds_snapshot_set_permissions",
+            region="us-east-2"
+        )
+        target_snapshot_id = "testing"
+        keep = "644160558196"
+        remove = "123456789012"
+        add = "234567890123"
+        policy = self.load_policy(
+            {
+                "name": "rds-snapshot-remove-permissions",
+                "resource": "rds-snapshot",
+                "source": "config",
+                "query": [
+                    {"clause": f"resourceId = '{target_snapshot_id}'"}],
+                "actions": [
+                    {"type": "set-permissions", "add": [add], "remove": [remove, "all"]}
+                ]
+            },
+            session_factory=session_factory,
+            config={"region": "us-east-2"},
+        )
+        client = session_factory().client("rds")
+        restore_permissions_before = self._get_effective_permissions(
+            client,
+            target_snapshot_id,
+        )
+        self.assertTrue({keep, remove, "all"}.issubset(restore_permissions_before))
+
+        resources = policy.run()
+        self.assertEqual(len(resources), 1)
+        restore_permissions_after = self._get_effective_permissions(
+            client,
+            target_snapshot_id,
+        )
+        self.assertTrue({keep, add}.issubset(restore_permissions_after))
+        self.assertEqual({remove, "all"}.intersection(restore_permissions_after), set())
+
+    def test_remove_matched_permissions(self):
+        session_factory = self.replay_flight_data(
+            "test_rds_snapshot_remove_matched_permissions",
+            region="us-east-2"
+        )
+        target_snapshot_id = "testing"
+        keep = "644160558196"
+        remove = "123456789012"
+        policy = self.load_policy(
+            {
+                "name": "rds-snapshot-remove-matched-permissions",
+                "resource": "rds-snapshot",
+                "source": "config",
+                "query": [
+                    {"clause": f"resourceId = '{target_snapshot_id}'"}],
+                "filters": [
+                    {"type": "cross-account", "whitelist": [keep]},
+                ],
+                "actions": [
+                    {"type": "set-permissions", "remove": "matched"}
+                ]
+            },
+            session_factory=session_factory,
+            config={"region": "us-east-2"},
+        )
+        resources = policy.run()
+        self.assertEqual(len(resources), 1)
+        restore_permissions_before = set(resources[0]["c7n:attributes"]["restore"])
+        self.assertTrue({keep, remove}.issubset(restore_permissions_before))
+
+        restore_permissions_after = self._get_effective_permissions(
+            session_factory().client("rds"),
+            resources[0]["DBSnapshotIdentifier"]
+        )
+        self.assertIn(keep, restore_permissions_after)
+        self.assertNotIn(remove, restore_permissions_after)
+
+    def test_clear_permissions(self):
+        session_factory = self.replay_flight_data(
+            "test_rds_snapshot_clear_permissions",
+            region="us-east-2"
+        )
+        target_snapshot_id = "testing"
+        policy = self.load_policy(
+            {
+                "name": "rds-snapshot-clear-permissions",
+                "resource": "rds-snapshot",
+                "source": "config",
+                "query": [
+                    {"clause": f"resourceId = '{target_snapshot_id}'"}],
+                "actions": [
+                    {"type": "set-permissions"}
+                ]
+            },
+            session_factory=session_factory,
+            config={"region": "us-east-2"},
+        )
+        resources = policy.run()
+        self.assertEqual(len(resources), 1)
+        restore_permissions_before = set(resources[0]["c7n:attributes"]["restore"])
+        self.assertGreater(len(restore_permissions_before), 0)
+
+        restore_permissions_after = self._get_effective_permissions(
+            session_factory().client("rds"),
+            resources[0]["DBSnapshotIdentifier"]
+        )
+        self.assertEqual(len(restore_permissions_after), 0)
+
+    def test_set_permissions_invalid(self):
+        with self.assertRaises(PolicyValidationError) as err:
+            self.load_policy(
+                {
+                    "name": "rds-snapshot-set-permissions-invalid",
+                    "resource": "rds-snapshot",
+                    "actions": [
+                        {"type": "set-permissions", "remove": "matched"}
+                    ]
+                },
+            )
+        self.assertIn("requires cross-account filter", str(err.exception))
+
 
 class TestModifyVpcSecurityGroupsAction(BaseTest):
 
diff --git a/tests/test_rdscluster.py b/tests/test_rdscluster.py
--- a/tests/test_rdscluster.py
+++ b/tests/test_rdscluster.py
@@ -512,3 +512,120 @@ def test_rdscluster_snapshot_trim(self):
         )
         resources = p.run()
         self.assertEqual(len(resources), 2)
+
+    def _get_effective_permissions(self, client, snapshot_id):
+        attributes = client.describe_db_cluster_snapshot_attributes(
+            DBClusterSnapshotIdentifier=snapshot_id
+        )["DBClusterSnapshotAttributesResult"]["DBClusterSnapshotAttributes"]
+        attr_map = {
+            attr["AttributeName"]: attr["AttributeValues"]
+            for attr in attributes
+        }
+        return set(attr_map.get("restore", []))
+
+    def test_set_permissions(self):
+        session_factory = self.replay_flight_data(
+            "test_rdscluster_snapshot_set_permissions",
+            region="us-east-2"
+        )
+        target_snapshot_id = "test"
+        keep = "644160558196"
+        remove = "123456789012"
+        add = "234567890123"
+        policy = self.load_policy(
+            {
+                "name": "rds-snapshot-remove-cross-account",
+                "resource": "rds-cluster-snapshot",
+                "source": "config",
+                "query": [
+                    {"clause": f"resourceId = '{target_snapshot_id}'"}],
+                "actions": [
+                    {"type": "set-permissions", "add": [add], "remove": [remove, "all"]}
+                ]
+            },
+            session_factory=session_factory,
+            config={"region": "us-east-2"},
+        )
+        client = session_factory().client("rds")
+        restore_permissions_before = self._get_effective_permissions(
+            client,
+            target_snapshot_id,
+        )
+        self.assertTrue({keep, remove, "all"}.issubset(restore_permissions_before))
+
+        resources = policy.run()
+        self.assertEqual(len(resources), 1)
+        restore_permissions_after = self._get_effective_permissions(
+            client,
+            target_snapshot_id,
+        )
+        self.assertTrue({keep, add}.issubset(restore_permissions_after))
+        self.assertEqual({remove, "all"}.intersection(restore_permissions_after), set())
+
+    def test_remove_matched_permissions(self):
+        session_factory = self.replay_flight_data(
+            "test_rdscluster_snapshot_remove_matched_permissions",
+            region="us-east-2"
+        )
+        target_snapshot_id = "test"
+        keep = "644160558196"
+        remove = "123456789012"
+        policy = self.load_policy(
+            {
+                "name": "rds-snapshot-remove-cross-account",
+                "resource": "rds-cluster-snapshot",
+                "source": "config",
+                "query": [
+                    {"clause": f"resourceId = '{target_snapshot_id}'"}],
+                "filters": [
+                    {"type": "cross-account", "whitelist": [keep]},
+                ],
+                "actions": [
+                    {"type": "set-permissions", "remove": "matched"}
+                ]
+            },
+            session_factory=session_factory,
+            config={"region": "us-east-2"},
+        )
+        resources = policy.run()
+        self.assertEqual(len(resources), 1)
+        restore_permissions_before = set(resources[0]["c7n:attributes"]["restore"])
+        self.assertTrue({keep, remove}.issubset(restore_permissions_before))
+
+        restore_permissions_after = self._get_effective_permissions(
+            session_factory().client("rds"),
+            resources[0]["DBClusterSnapshotIdentifier"]
+        )
+        self.assertIn(keep, restore_permissions_after)
+        self.assertNotIn(remove, restore_permissions_after)
+
+    def test_clear_permissions(self):
+        session_factory = self.replay_flight_data(
+            "test_rdscluster_snapshot_clear_permissions",
+            region="us-east-2"
+        )
+        target_snapshot_id = "test"
+        policy = self.load_policy(
+            {
+                "name": "rds-snapshot-remove-cross-account",
+                "resource": "rds-cluster-snapshot",
+                "source": "config",
+                "query": [
+                    {"clause": f"resourceId = '{target_snapshot_id}'"}],
+                "actions": [
+                    {"type": "set-permissions"}
+                ]
+            },
+            session_factory=session_factory,
+            config={"region": "us-east-2"},
+        )
+        resources = policy.run()
+        self.assertEqual(len(resources), 1)
+        restore_permissions_before = set(resources[0]["c7n:attributes"]["restore"])
+        self.assertGreater(len(restore_permissions_before), 0)
+
+        restore_permissions_after = self._get_effective_permissions(
+            session_factory().client("rds"),
+            resources[0]["DBClusterSnapshotIdentifier"]
+        )
+        self.assertEqual(len(restore_permissions_after), 0)