Repository: cloud-custodian/cloud-custodian

Issue:
aws.dynamodb - Add deletion-protection-enabled action
### Describe the feature

I'd like to take advantage of enabling deletion protection for DynamoDBs

Perhaps this would be built into the [`update` action](https://cloudcustodian.io/docs/aws/resources/dynamodb-table.html#update) ?

e.g.

```yaml
policies:
  - name: dynamodb-set-deletion-protection
    resource: aws.dynamodb-table
    actions:
      - type: update
        DeletionProtectionEnabled: true
```

Code reference to `update` action's schema

https://github.com/cloud-custodian/cloud-custodian/blob/7be74ff24fe0a06463eb60cd9c1023b11e6710df/c7n/resources/dynamodb.py#L183-L189

Code reference to `update` action's method of passing in params directly to `update_item`

https://github.com/cloud-custodian/cloud-custodian/blob/7be74ff24fe0a06463eb60cd9c1023b11e6710df/c7n/resources/dynamodb.py#L200-L202

Could be as simple as updating the schema here and adding a test

### Extra information or context

- https://aws.amazon.com/about-aws/whats-new/2023/03/amazon-dynamodb-table-deletion-protection/
- https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb/client/update_table.html
- https://docs.aws.amazon.com/cli/latest/reference/dynamodb/update-table.html
- Related PR for adding the delete action to dynamodb https://github.com/cloud-custodian/cloud-custodian/pull/773/files
- Related PR for adding deletion protection support https://github.com/cloud-custodian/cloud-custodian/issues/3693


Discussion:
++  We would also benefit from the ability to remove Deletion Protection from Dynamo-DB tables. Custodian Delete action fails (of course) when Deletion Protection is enabled. It would be great to have a method to enable or disable Deletion Protection. A Force Delete option would also be helpful.

Resolving patch:
diff --git a/c7n/resources/dynamodb.py b/c7n/resources/dynamodb.py
--- a/c7n/resources/dynamodb.py
+++ b/c7n/resources/dynamodb.py
@@ -183,6 +183,7 @@ class UpdateTable(BaseAction):
     schema = type_schema(
         'update',
         BillingMode={'enum': ['PROVISIONED', 'PAY_PER_REQUEST']},
+        DeletionProtectionEnabled={'enum': [True, False]},
         ProvisionedThroughput={'type': 'object',
             'properties': {
                 'ReadCapacityUnits': {'type': 'integer'},
@@ -222,10 +223,15 @@ class DeleteTable(BaseAction):
     """
 
     valid_status = ('ACTIVE',)
-    schema = type_schema('delete')
-    permissions = ("dynamodb:DeleteTable",)
+    schema = type_schema('delete',
+        force={'type': 'boolean', 'default': False})
+    permissions = ("dynamodb:UpdateTable", "dynamodb:DeleteTable",)
 
     def delete_table(self, client, table_set):
+        if self.data.get('force', False):
+            del_protection_updater = self.manager.action_registry['update'](
+                {'type': 'update', 'DeletionProtectionEnabled': False}, self.manager)
+            del_protection_updater.process(table_set)
         for t in table_set:
             client.delete_table(TableName=t['TableName'])
 


Test patch:
diff --git a/tests/data/placebo/test_force_delete_dynamodb_tables/dynamodb.DeleteTable_1.json b/tests/data/placebo/test_force_delete_dynamodb_tables/dynamodb.DeleteTable_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_force_delete_dynamodb_tables/dynamodb.DeleteTable_1.json
@@ -0,0 +1,36 @@
+{
+    "status_code": 200,
+    "data": {
+        "TableDescription": {
+            "TableName": "c7n-test",
+            "TableStatus": "DELETING",
+            "ProvisionedThroughput": {
+                "NumberOfDecreasesToday": 0,
+                "ReadCapacityUnits": 0,
+                "WriteCapacityUnits": 0
+            },
+            "TableSizeBytes": 0,
+            "ItemCount": 0,
+            "TableArn": "arn:aws:dynamodb:us-east-1:644160558196:table/c7n-test",
+            "TableId": "51a61234-5d77-43d7-8eaf-642b32bc241a",
+            "BillingModeSummary": {
+                "BillingMode": "PAY_PER_REQUEST",
+                "LastUpdateToPayPerRequestDateTime": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 10,
+                    "day": 30,
+                    "hour": 23,
+                    "minute": 22,
+                    "second": 56,
+                    "microsecond": 582000
+                }
+            },
+            "TableClassSummary": {
+                "TableClass": "STANDARD_INFREQUENT_ACCESS"
+            },
+            "DeletionProtectionEnabled": false
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_force_delete_dynamodb_tables/dynamodb.DescribeTable_1.json b/tests/data/placebo/test_force_delete_dynamodb_tables/dynamodb.DescribeTable_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_force_delete_dynamodb_tables/dynamodb.DescribeTable_1.json
@@ -0,0 +1,58 @@
+{
+    "status_code": 200,
+    "data": {
+        "Table": {
+            "AttributeDefinitions": [
+                {
+                    "AttributeName": "Name",
+                    "AttributeType": "S"
+                }
+            ],
+            "TableName": "c7n-test",
+            "KeySchema": [
+                {
+                    "AttributeName": "Name",
+                    "KeyType": "HASH"
+                }
+            ],
+            "TableStatus": "ACTIVE",
+            "CreationDateTime": {
+                "__class__": "datetime",
+                "year": 2023,
+                "month": 10,
+                "day": 30,
+                "hour": 23,
+                "minute": 22,
+                "second": 56,
+                "microsecond": 582000
+            },
+            "ProvisionedThroughput": {
+                "NumberOfDecreasesToday": 0,
+                "ReadCapacityUnits": 0,
+                "WriteCapacityUnits": 0
+            },
+            "TableSizeBytes": 0,
+            "ItemCount": 0,
+            "TableArn": "arn:aws:dynamodb:us-east-1:644160558196:table/c7n-test",
+            "TableId": "51a61234-5d77-43d7-8eaf-642b32bc241a",
+            "BillingModeSummary": {
+                "BillingMode": "PAY_PER_REQUEST",
+                "LastUpdateToPayPerRequestDateTime": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 10,
+                    "day": 30,
+                    "hour": 23,
+                    "minute": 22,
+                    "second": 56,
+                    "microsecond": 582000
+                }
+            },
+            "TableClassSummary": {
+                "TableClass": "STANDARD_INFREQUENT_ACCESS"
+            },
+            "DeletionProtectionEnabled": true
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_force_delete_dynamodb_tables/dynamodb.DescribeTable_2.json b/tests/data/placebo/test_force_delete_dynamodb_tables/dynamodb.DescribeTable_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_force_delete_dynamodb_tables/dynamodb.DescribeTable_2.json
@@ -0,0 +1,36 @@
+{
+    "status_code": 200,
+    "data": {
+        "Table": {
+            "TableName": "c7n-test",
+            "TableStatus": "DELETING",
+            "ProvisionedThroughput": {
+                "NumberOfDecreasesToday": 0,
+                "ReadCapacityUnits": 0,
+                "WriteCapacityUnits": 0
+            },
+            "TableSizeBytes": 0,
+            "ItemCount": 0,
+            "TableArn": "arn:aws:dynamodb:us-east-1:644160558196:table/c7n-test",
+            "TableId": "51a61234-5d77-43d7-8eaf-642b32bc241a",
+            "BillingModeSummary": {
+                "BillingMode": "PAY_PER_REQUEST",
+                "LastUpdateToPayPerRequestDateTime": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 10,
+                    "day": 30,
+                    "hour": 23,
+                    "minute": 22,
+                    "second": 56,
+                    "microsecond": 582000
+                }
+            },
+            "TableClassSummary": {
+                "TableClass": "STANDARD_INFREQUENT_ACCESS"
+            },
+            "DeletionProtectionEnabled": false
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_force_delete_dynamodb_tables/dynamodb.ListTables_1.json b/tests/data/placebo/test_force_delete_dynamodb_tables/dynamodb.ListTables_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_force_delete_dynamodb_tables/dynamodb.ListTables_1.json
@@ -0,0 +1,9 @@
+{
+    "status_code": 200,
+    "data": {
+        "TableNames": [
+            "c7n-test"
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_force_delete_dynamodb_tables/dynamodb.UpdateTable_1.json b/tests/data/placebo/test_force_delete_dynamodb_tables/dynamodb.UpdateTable_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_force_delete_dynamodb_tables/dynamodb.UpdateTable_1.json
@@ -0,0 +1,58 @@
+{
+    "status_code": 200,
+    "data": {
+        "TableDescription": {
+            "AttributeDefinitions": [
+                {
+                    "AttributeName": "Name",
+                    "AttributeType": "S"
+                }
+            ],
+            "TableName": "c7n-test",
+            "KeySchema": [
+                {
+                    "AttributeName": "Name",
+                    "KeyType": "HASH"
+                }
+            ],
+            "TableStatus": "ACTIVE",
+            "CreationDateTime": {
+                "__class__": "datetime",
+                "year": 2023,
+                "month": 10,
+                "day": 30,
+                "hour": 23,
+                "minute": 22,
+                "second": 56,
+                "microsecond": 582000
+            },
+            "ProvisionedThroughput": {
+                "NumberOfDecreasesToday": 0,
+                "ReadCapacityUnits": 0,
+                "WriteCapacityUnits": 0
+            },
+            "TableSizeBytes": 0,
+            "ItemCount": 0,
+            "TableArn": "arn:aws:dynamodb:us-east-1:644160558196:table/c7n-test",
+            "TableId": "51a61234-5d77-43d7-8eaf-642b32bc241a",
+            "BillingModeSummary": {
+                "BillingMode": "PAY_PER_REQUEST",
+                "LastUpdateToPayPerRequestDateTime": {
+                    "__class__": "datetime",
+                    "year": 2023,
+                    "month": 10,
+                    "day": 30,
+                    "hour": 23,
+                    "minute": 22,
+                    "second": 56,
+                    "microsecond": 582000
+                }
+            },
+            "TableClassSummary": {
+                "TableClass": "STANDARD_INFREQUENT_ACCESS"
+            },
+            "DeletionProtectionEnabled": false
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_force_delete_dynamodb_tables/tagging.GetResources_1.json b/tests/data/placebo/test_force_delete_dynamodb_tables/tagging.GetResources_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_force_delete_dynamodb_tables/tagging.GetResources_1.json
@@ -0,0 +1,18 @@
+{
+    "status_code": 200,
+    "data": {
+        "PaginationToken": "",
+        "ResourceTagMappingList": [
+            {
+                "ResourceARN": "arn:aws:dynamodb:us-east-1:644160558196:table/c7n-test",
+                "Tags": [
+                    {
+                        "Key": "Owner",
+                        "Value": "custodian"
+                    }
+                ]
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/test_dynamodb.py b/tests/test_dynamodb.py
--- a/tests/test_dynamodb.py
+++ b/tests/test_dynamodb.py
@@ -54,6 +54,29 @@ def test_delete_tables(self):
         resources = p.run()
         self.assertEqual(resources[0]["TableName"], "c7n.DynamoDB.01")
 
+    def test_force_delete_dynamodb_tables(self):
+        session_factory = self.replay_flight_data("test_force_delete_dynamodb_tables")
+        client = session_factory().client("dynamodb")
+        self.patch(DeleteTable, "executor_factory", MainThreadExecutor)
+        p = self.load_policy(
+            {
+                "name": "delete-empty-tables",
+                "resource": "dynamodb-table",
+                "filters": [{"TableName": "c7n-test"}],
+                "actions": [
+                    {
+                        "type": "delete",
+                        "force": True
+                    }
+                ],
+            },
+            session_factory=session_factory,
+        )
+        resources = p.run()
+        self.assertEqual(resources[0]["DeletionProtectionEnabled"], True)
+        table = client.describe_table(TableName="c7n-test")["Table"]
+        self.assertEqual(table.get('TableStatus'), 'DELETING')
+
     def test_update_tables(self):
         session_factory = self.replay_flight_data("test_dynamodb_update_table")
         client = session_factory().client("dynamodb")