Repository: cloud-custodian/cloud-custodian

Issue:
Set Athena Workgroup Encryption also tries to change readonly field
### Describe the bug

Hi, we are currently trying to set the encryption for our primary athena workgroups that are unencrypted. The policy looks like this:

```yaml
- name: set-athena-workgroup-encryption
  resource: awscc.athena_workgroup
  filters:
    - type: value
      key: Name
      value: "primary"
    - type: value
      key:  "WorkGroupConfiguration.ResultConfiguration.EncryptionConfiguration.EncryptionOption"
      value: absent
  actions:
    - type: update
      WorkGroupConfiguration:
        EnforceWorkGroupConfiguration: true
    - type: update
      WorkGroupConfiguration:
        ResultConfiguration:
          EncryptionConfiguration:
            EncryptionOption: SSE_S3
```

When executing this policy we get this error though: 

```
2024-02-15 09:59:25,626: custodian.commands:ERROR Error while executing policy set-athena-workgroup-encryption, continuing
Traceback (most recent call last):
  File "/home/.../policies/.venv/lib/python3.11/site-packages/c7n/commands.py", line 307, in run
    policy()
  File "/home/.../policies/.venv/lib/python3.11/site-packages/c7n/policy.py", line 1357, in __call__
    resources = mode.run()
                ^^^^^^^^^^
  File "/home/.../policies/.venv/lib/python3.11/site-packages/c7n/policy.py", line 364, in run
    results = a.process(resources)
              ^^^^^^^^^^^^^^^^^^^^
  File "/home/.../policies/.venv/lib/python3.11/site-packages/c7n_awscc/actions.py", line 43, in process
    client.update_resource(
  File "/home/.../policies/.venv/lib/python3.11/site-packages/botocore/client.py", line 553, in _api_call
    return self._make_api_call(operation_name, kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/.../policies/.venv/lib/python3.11/site-packages/botocore/client.py", line 1009, in _make_api_call
    raise error_class(parsed_response, operation_name)
botocore.exceptions.ClientError: An error occurred (ValidationException) when calling the UpdateResource operation: Invalid patch update: readOnlyProperties [/properties/WorkGroupConfiguration/EngineVersion/EffectiveEngineVersion] cannot be updated
2024-02-15 09:59:25,627: custodian.commands:ERROR The following policies had errors while executing
 - set-athena-workgroup-encryption
```

But we don't want to update the engine version itself.

### What did you expect to happen?

We expected the policy to update the encryption setting and not touch the engine version, because the attribute was not specified in our policy

### Cloud Provider

Amazon Web Services (AWS)

### Cloud Custodian version and dependency information

```shell
Custodian:   0.9.34
Python:      3.11.4 (main, Dec  7 2023, 15:43:41) [GCC 12.3.0]
Platform:    posix.uname_result(sysname='Linux', nodename='marcel', release='6.2.0-39-generic', version='#40-Ubuntu SMP PREEMPT_DYNAMIC Tue Nov 14 14:18:00 UTC 2023', machine='x86_64')
Using venv:  True
Docker: False
Installed:

argcomplete==3.2.1
attrs==23.2.0
boto3==1.34.21
botocore==1.34.21
docutils==0.18.1
importlib-metadata==6.11.0
jmespath==1.0.1
jsonschema==4.21.0
jsonschema-specifications==2023.12.1
python-dateutil==2.8.2
pyyaml==6.0.1
referencing==0.31.1
rpds-py==0.17.1
s3transfer==0.10.0
six==1.16.0
tabulate==0.9.0
urllib3==1.26.18
zipp==3.17.0
```


### Policy

```shell
- name: set-athena-workgroup-encryption
  resource: awscc.athena_workgroup
  filters:
    - type: value
      key: Name
      value: "primary"
    - type: value
      key:  "WorkGroupConfiguration.ResultConfiguration.EncryptionConfiguration.EncryptionOption"
      value: absent
  actions:
    - type: update
      WorkGroupConfiguration:
        EnforceWorkGroupConfiguration: true
    - type: update
      WorkGroupConfiguration:
        ResultConfiguration:
          EncryptionConfiguration:
            EncryptionOption: SSE_S3
```


### Relevant log/traceback output

```shell
2024-02-15 09:59:25,626: custodian.commands:ERROR Error while executing policy set-athena-workgroup-encryption, continuing
Traceback (most recent call last):
  File "/home/.../policies/.venv/lib/python3.11/site-packages/c7n/commands.py", line 307, in run
    policy()
  File "/home/.../policies/.venv/lib/python3.11/site-packages/c7n/policy.py", line 1357, in __call__
    resources = mode.run()
                ^^^^^^^^^^
  File "/home/.../policies/.venv/lib/python3.11/site-packages/c7n/policy.py", line 364, in run
    results = a.process(resources)
              ^^^^^^^^^^^^^^^^^^^^
  File "/home/.../policies/.venv/lib/python3.11/site-packages/c7n_awscc/actions.py", line 43, in process
    client.update_resource(
  File "/home/.../policies/.venv/lib/python3.11/site-packages/botocore/client.py", line 553, in _api_call
    return self._make_api_call(operation_name, kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/.../policies/.venv/lib/python3.11/site-packages/botocore/client.py", line 1009, in _make_api_call
    raise error_class(parsed_response, operation_name)
botocore.exceptions.ClientError: An error occurred (ValidationException) when calling the UpdateResource operation: Invalid patch update: readOnlyProperties [/properties/WorkGroupConfiguration/EngineVersion/EffectiveEngineVersion] cannot be updated
2024-02-15 09:59:25,627: custodian.commands:ERROR The following policies had errors while executing
 - set-athena-workgroup-encryption
```


### Extra information or context

We tried to use the update attributes like this 

```yaml
    - type: update
      WorkGroupConfigurationUpdates:
        EnforceWorkGroupConfiguration: true
        ResultConfigurationUpdates:
          EncryptionConfiguration:
            EncryptionOption: SSE_S3
```

but there is currently a bug in AWS which resets the workgroup right after the operation. We are in communication with AWS Support there, but in the meantime we tried to make it work with the approach described above.


Discussion:

Resolving patch:
diff --git a/tools/c7n_awscc/c7n_awscc/actions.py b/tools/c7n_awscc/c7n_awscc/actions.py
--- a/tools/c7n_awscc/c7n_awscc/actions.py
+++ b/tools/c7n_awscc/c7n_awscc/actions.py
@@ -33,8 +33,16 @@ def process(self, resources):
 
 
 class Update(ControlAction):
+    """Update a resource.
+
+    Supports either whole key/value replacement via an attribute mapping
+    or jsonpatch `patch:`
+    """
+
     # schema is setup at resource type initialization
 
+    _action_meta = ("type", "patch")
+
     def process(self, resources):
         client = local_session(self.manager.session_factory).client("cloudcontrol")
         for r in resources:
@@ -46,9 +54,23 @@ def process(self, resources):
             )
 
     def get_patch(self, r):
-        tgt = dict(r)
+        # we support either using json patch to do a partial modification.
+        if self.data.get("patch"):
+            return jsonpatch.JsonPatch(self.data["patch"])
+
+        current = dict(r)
+
+        # the action's schema reflects updatable properties
+        updatable = {k for k in self.schema["properties"] if k not in self._action_meta}
+        for k in list(set(current) - updatable):
+            del current[k]
+
+        # shallow copy for patch generation
+        tgt = dict(current)
+
+        # or whole key value replacement.
         for k, v in self.data.items():
             if k == "type":
                 continue
             tgt[k] = v
-        return jsonpatch.make_patch(r, tgt)
+        return jsonpatch.make_patch(current, tgt)
diff --git a/tools/c7n_awscc/c7n_awscc/manager.py b/tools/c7n_awscc/c7n_awscc/manager.py
--- a/tools/c7n_awscc/c7n_awscc/manager.py
+++ b/tools/c7n_awscc/c7n_awscc/manager.py
@@ -112,6 +112,19 @@ def get_update_schema(schema, rname):
         "properties": {u: schema["properties"][u] for u in updatable},
     }
     update_schema["properties"]["type"] = {"enum": ["update"]}
+    update_schema["properties"]["patch"] = {
+        # This schema is pretty minimal
+        "description": "Json patch to apply to resources",
+        "type": "array",
+        "items": {
+            "type": "object",
+            "required": ["op", "path"],
+            "properties": {
+                "path": {"type": "string"},
+                "op": {"enum": ["add", "remove", "update", "replace", "move", "copy", "test"]},
+            },
+        },
+    }
 
     if "definitions" in schema:
         update_schema["definitions"] = dict(schema["definitions"])


Test patch:
diff --git a/tools/c7n_awscc/tests/data/placebo/awscc_workgroup_update/athena.GetWorkGroup_1.json b/tools/c7n_awscc/tests/data/placebo/awscc_workgroup_update/athena.GetWorkGroup_1.json
new file mode 100644
--- /dev/null
+++ b/tools/c7n_awscc/tests/data/placebo/awscc_workgroup_update/athena.GetWorkGroup_1.json
@@ -0,0 +1,36 @@
+{
+    "status_code": 200,
+    "data": {
+        "WorkGroup": {
+            "Name": "tf-test-loving-llama",
+            "State": "ENABLED",
+            "Configuration": {
+                "ResultConfiguration": {
+                    "EncryptionConfiguration": {
+                        "EncryptionOption": "SSE_S3"
+                    }
+                },
+                "EnforceWorkGroupConfiguration": false,
+                "PublishCloudWatchMetricsEnabled": true,
+                "RequesterPaysEnabled": false,
+                "EngineVersion": {
+                    "SelectedEngineVersion": "AUTO",
+                    "EffectiveEngineVersion": "Athena engine version 3"
+                },
+                "EnableMinimumEncryptionConfiguration": false
+            },
+            "Description": "",
+            "CreationTime": {
+                "__class__": "datetime",
+                "year": 2024,
+                "month": 2,
+                "day": 21,
+                "hour": 13,
+                "minute": 14,
+                "second": 11,
+                "microsecond": 444000
+            }
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tools/c7n_awscc/tests/data/placebo/awscc_workgroup_update/cloudcontrolapi.GetResource_1.json b/tools/c7n_awscc/tests/data/placebo/awscc_workgroup_update/cloudcontrolapi.GetResource_1.json
new file mode 100644
--- /dev/null
+++ b/tools/c7n_awscc/tests/data/placebo/awscc_workgroup_update/cloudcontrolapi.GetResource_1.json
@@ -0,0 +1,11 @@
+{
+    "status_code": 200,
+    "data": {
+        "TypeName": "AWS::Athena::WorkGroup",
+        "ResourceDescription": {
+            "Identifier": "primary",
+            "Properties": "{\"WorkGroupConfiguration\":{\"EnforceWorkGroupConfiguration\":false,\"EngineVersion\":{\"SelectedEngineVersion\":\"AUTO\",\"EffectiveEngineVersion\":\"Athena engine version 3\"},\"PublishCloudWatchMetricsEnabled\":false,\"ResultConfiguration\":{},\"RequesterPaysEnabled\":false},\"State\":\"ENABLED\",\"CreationTime\":\"1642008392\",\"Tags\":[],\"Name\":\"primary\"}"
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tools/c7n_awscc/tests/data/placebo/awscc_workgroup_update/cloudcontrolapi.GetResource_2.json b/tools/c7n_awscc/tests/data/placebo/awscc_workgroup_update/cloudcontrolapi.GetResource_2.json
new file mode 100644
--- /dev/null
+++ b/tools/c7n_awscc/tests/data/placebo/awscc_workgroup_update/cloudcontrolapi.GetResource_2.json
@@ -0,0 +1,11 @@
+{
+    "status_code": 200,
+    "data": {
+        "TypeName": "AWS::Athena::WorkGroup",
+        "ResourceDescription": {
+            "Identifier": "tf-test-loving-llama",
+            "Properties": "{\"WorkGroupConfiguration\":{\"EnforceWorkGroupConfiguration\":true,\"EngineVersion\":{\"SelectedEngineVersion\":\"AUTO\",\"EffectiveEngineVersion\":\"Athena engine version 3\"},\"PublishCloudWatchMetricsEnabled\":true,\"RequesterPaysEnabled\":false},\"State\":\"ENABLED\",\"CreationTime\":\"1708539251\",\"Tags\":[],\"Name\":\"tf-test-loving-llama\"}"
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tools/c7n_awscc/tests/data/placebo/awscc_workgroup_update/cloudcontrolapi.ListResources_1.json b/tools/c7n_awscc/tests/data/placebo/awscc_workgroup_update/cloudcontrolapi.ListResources_1.json
new file mode 100644
--- /dev/null
+++ b/tools/c7n_awscc/tests/data/placebo/awscc_workgroup_update/cloudcontrolapi.ListResources_1.json
@@ -0,0 +1,17 @@
+{
+    "status_code": 200,
+    "data": {
+        "TypeName": "AWS::Athena::WorkGroup",
+        "ResourceDescriptions": [
+            {
+                "Identifier": "primary",
+                "Properties": "{\"Description\":\"\",\"State\":\"ENABLED\",\"CreationTime\":\"1642008392\",\"Name\":\"primary\"}"
+            },
+            {
+                "Identifier": "tf-test-loving-llama",
+                "Properties": "{\"Description\":\"\",\"State\":\"ENABLED\",\"CreationTime\":\"1708539251\",\"Name\":\"tf-test-loving-llama\"}"
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tools/c7n_awscc/tests/data/placebo/awscc_workgroup_update/cloudcontrolapi.UpdateResource_1.json b/tools/c7n_awscc/tests/data/placebo/awscc_workgroup_update/cloudcontrolapi.UpdateResource_1.json
new file mode 100644
--- /dev/null
+++ b/tools/c7n_awscc/tests/data/placebo/awscc_workgroup_update/cloudcontrolapi.UpdateResource_1.json
@@ -0,0 +1,24 @@
+{
+    "status_code": 200,
+    "data": {
+        "ProgressEvent": {
+            "TypeName": "AWS::Athena::WorkGroup",
+            "Identifier": "tf-test-loving-llama",
+            "RequestToken": "f76d664b-9097-4c9e-b2fb-68cecf8faa50",
+            "Operation": "UPDATE",
+            "OperationStatus": "IN_PROGRESS",
+            "EventTime": {
+                "__class__": "datetime",
+                "year": 2024,
+                "month": 2,
+                "day": 21,
+                "hour": 13,
+                "minute": 14,
+                "second": 16,
+                "microsecond": 337000
+            },
+            "ResourceModel": "{\"WorkGroupConfiguration\":{\"EnforceWorkGroupConfiguration\":false,\"EngineVersion\":{\"SelectedEngineVersion\":\"AUTO\"},\"PublishCloudWatchMetricsEnabled\":true,\"ResultConfiguration\":{\"EncryptionConfiguration\":{\"EncryptionOption\":\"SSE_S3\"}},\"RequesterPaysEnabled\":false},\"State\":\"ENABLED\",\"Tags\":[],\"Name\":\"tf-test-loving-llama\"}"
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tools/c7n_awscc/tests/terraform/aws_athena_workgroup/main.tf b/tools/c7n_awscc/tests/terraform/aws_athena_workgroup/main.tf
new file mode 100644
--- /dev/null
+++ b/tools/c7n_awscc/tests/terraform/aws_athena_workgroup/main.tf
@@ -0,0 +1,13 @@
+resource "random_pet" "names" {
+  prefix = "tf-test"
+  length = 2
+}
+
+
+resource "aws_athena_workgroup" "example" {
+  name = random_pet.names.id
+
+  configuration {
+    publish_cloudwatch_metrics_enabled = true
+  }
+}
diff --git a/tools/c7n_awscc/tests/terraform/aws_athena_workgroup/tf_resources.json b/tools/c7n_awscc/tests/terraform/aws_athena_workgroup/tf_resources.json
new file mode 100644
--- /dev/null
+++ b/tools/c7n_awscc/tests/terraform/aws_athena_workgroup/tf_resources.json
@@ -0,0 +1,43 @@
+{
+    "pytest-terraform": 1,
+    "outputs": {},
+    "resources": {
+        "aws_athena_workgroup": {
+            "example": {
+                "arn": "arn:aws:athena:us-east-1:490065885863:workgroup/tf-test-loving-llama",
+                "configuration": [
+                    {
+                        "bytes_scanned_cutoff_per_query": 0,
+                        "enforce_workgroup_configuration": true,
+                        "engine_version": [
+                            {
+                                "effective_engine_version": "Athena engine version 3",
+                                "selected_engine_version": "AUTO"
+                            }
+                        ],
+                        "execution_role": "",
+                        "publish_cloudwatch_metrics_enabled": true,
+                        "requester_pays_enabled": false,
+                        "result_configuration": []
+                    }
+                ],
+                "description": "",
+                "force_destroy": false,
+                "id": "tf-test-loving-llama",
+                "name": "tf-test-loving-llama",
+                "state": "ENABLED",
+                "tags": null,
+                "tags_all": {}
+            }
+        },
+        "random_pet": {
+            "names": {
+                "id": "tf-test-loving-llama",
+                "keepers": null,
+                "length": 2,
+                "prefix": "tf-test",
+                "separator": "-"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tools/c7n_awscc/tests/test_ccaction.py b/tools/c7n_awscc/tests/test_ccaction.py
--- a/tools/c7n_awscc/tests/test_ccaction.py
+++ b/tools/c7n_awscc/tests/test_ccaction.py
@@ -2,6 +2,8 @@
 # SPDX-License-Identifier: Apache-2.0
 import time
 
+from pytest_terraform import terraform
+
 
 def test_delete(test_awscc):
     factory = test_awscc.replay_flight_data("awscc_log_delete")
@@ -28,6 +30,51 @@ def test_delete(test_awscc):
     )
 
 
+@terraform("aws_athena_workgroup", replay=True)
+def test_update_workgroup(test_awscc, aws_athena_workgroup):
+    factory = test_awscc.replay_flight_data("awscc_workgroup_update")
+    p = test_awscc.load_policy(
+        {
+            "name": "work-up",
+            "resource": "awscc.athena_workgroup",
+            "filters": [{"Name": aws_athena_workgroup["aws_athena_workgroup.example.name"]}],
+            "actions": [
+                {
+                    "type": "update",
+                    "patch": [
+                        dict(
+                            op="add",
+                            path="/WorkGroupConfiguration/EnforceWorkGroupConfiguration",
+                            value=False,
+                        ),
+                        dict(
+                            op="add",
+                            path="/WorkGroupConfiguration/ResultConfiguration",
+                            value={"EncryptionConfiguration": {"EncryptionOption": "SSE_S3"}},
+                        ),
+                    ],
+                }
+            ],
+        },
+        session_factory=factory,
+    )
+
+    resources = p.run()
+    assert len(resources) == 1
+    client = factory().client('athena')
+    if test_awscc.recording:
+        time.sleep(4)
+
+    wg = client.get_work_group(
+        WorkGroup=aws_athena_workgroup["aws_athena_workgroup.example.name"]
+    ).get("WorkGroup")
+    assert wg["Configuration"]["EnforceWorkGroupConfiguration"] is False
+    assert (
+        wg["Configuration"]["ResultConfiguration"]["EncryptionConfiguration"]["EncryptionOption"]
+        == "SSE_S3"
+    )
+
+
 def test_update(test_awscc):
     factory = test_awscc.replay_flight_data("awscc_log_update")
     group_name = "/aws/codebuild/custodian-build-python"