From f7d769c6c29c9f5efb5ae847e0baa6e0df510098 Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Tue, 18 Aug 2026 09:58:50 +0000 Subject: [PATCH 01/12] Add per-resource permissions support to cluster_policies Cluster policies support a single ACL level, CAN_USE (SDK compute.ClusterPolicyPermissionLevel). Wire up the per-resource `permissions` field so a policy can grant CAN_USE to users, groups, and service principals via the direct engine's generic permissions applier: - Declare the ClusterPolicyPermission type and add the Permissions field. - Register cluster_policies.permissions in the direct engine and map the permissions-API object type to /cluster-policies/. - Regenerate the bundle JSON schema and fill in the field descriptions. cluster_policies is intentionally left out of the top-level levelsMap (it stays in unsupportedResources): the bundle-wide permissions block is expressed in CAN_MANAGE/CAN_VIEW/CAN_RUN, none of which a cluster policy can accept, so only the explicit per-resource form is supported. Co-authored-by: Isaac --- bundle/config/resources/cluster_policy.go | 2 + bundle/config/resources/permission_types.go | 1 + bundle/direct/dresources/all.go | 1 + bundle/direct/dresources/all_test.go | 10 +++ bundle/direct/dresources/permissions.go | 1 + bundle/internal/schema/annotations.yml | 20 ++++++ bundle/schema/jsonschema.json | 67 +++++++++++++++++++++ 7 files changed, 102 insertions(+) diff --git a/bundle/config/resources/cluster_policy.go b/bundle/config/resources/cluster_policy.go index 5c447b0beb9..b0b07ff5fb9 100644 --- a/bundle/config/resources/cluster_policy.go +++ b/bundle/config/resources/cluster_policy.go @@ -23,6 +23,8 @@ type ClusterPolicy struct { // Shadows the embedded compute.CreatePolicy.PolicyFamilyDefinitionOverrides (a string), // same as Definition: also a policy document authorable as inline YAML. PolicyFamilyDefinitionOverrides any `json:"policy_family_definition_overrides,omitempty"` + + Permissions []ClusterPolicyPermission `json:"permissions,omitempty"` } func (s *ClusterPolicy) UnmarshalJSON(b []byte) error { diff --git a/bundle/config/resources/permission_types.go b/bundle/config/resources/permission_types.go index d067c5e3e37..31d41e09439 100644 --- a/bundle/config/resources/permission_types.go +++ b/bundle/config/resources/permission_types.go @@ -27,6 +27,7 @@ func (p Permission) String() string { type ( AppPermission PermissionT[apps.AppPermissionLevel] ClusterPermission PermissionT[compute.ClusterPermissionLevel] + ClusterPolicyPermission PermissionT[compute.ClusterPolicyPermissionLevel] InstancePoolPermission PermissionT[compute.InstancePoolPermissionLevel] JobPermission PermissionT[jobs.JobPermissionLevel] MlflowExperimentPermission PermissionT[ml.ExperimentPermissionLevel] diff --git a/bundle/direct/dresources/all.go b/bundle/direct/dresources/all.go index 2c82df2aab9..391fb0684d2 100644 --- a/bundle/direct/dresources/all.go +++ b/bundle/direct/dresources/all.go @@ -48,6 +48,7 @@ var SupportedResources = map[string]any{ "apps.permissions": (*ResourcePermissions)(nil), "alerts.permissions": (*ResourcePermissions)(nil), "clusters.permissions": (*ResourcePermissions)(nil), + "cluster_policies.permissions": (*ResourcePermissions)(nil), "database_instances.permissions": (*ResourcePermissions)(nil), "postgres_projects.permissions": (*ResourcePermissions)(nil), "experiments.permissions": (*ResourcePermissions)(nil), diff --git a/bundle/direct/dresources/all_test.go b/bundle/direct/dresources/all_test.go index 75a94ffc88c..0c0d3d05ada 100644 --- a/bundle/direct/dresources/all_test.go +++ b/bundle/direct/dresources/all_test.go @@ -452,6 +452,16 @@ var testDeps = map[string]prepareWorkspace{ }, nil }, + "cluster_policies.permissions": func(ctx context.Context, client *databricks.WorkspaceClient) (any, error) { + return &PermissionsState{ + ObjectID: "/cluster-policies/cluster-policy-permissions", + EmbeddedSlice: []StatePermission{{ + Level: "CAN_USE", + UserName: "user@example.com", + }}, + }, nil + }, + "instance_pools.permissions": func(ctx context.Context, client *databricks.WorkspaceClient) (any, error) { return &PermissionsState{ ObjectID: "/instance-pools/pool-permissions", diff --git a/bundle/direct/dresources/permissions.go b/bundle/direct/dresources/permissions.go index aa838acfeef..2ab265d29f1 100644 --- a/bundle/direct/dresources/permissions.go +++ b/bundle/direct/dresources/permissions.go @@ -17,6 +17,7 @@ var permissionResourceToObjectType = map[string]string{ "alerts": "/alertsv2/", "apps": "/apps/", "clusters": "/clusters/", + "cluster_policies": "/cluster-policies/", "instance_pools": "/instance-pools/", "dashboards": "/dashboards/", "genie_spaces": "/genie/", diff --git a/bundle/internal/schema/annotations.yml b/bundle/internal/schema/annotations.yml index 96620ba9a89..e47f89c505a 100644 --- a/bundle/internal/schema/annotations.yml +++ b/bundle/internal/schema/annotations.yml @@ -550,6 +550,26 @@ resources: "lifecycle": "description": |- PLACEHOLDER + "permissions": + "description": |- + The permissions to apply to this resource. + "markdown_description": |- + A Sequence of permissions to apply to this resource, where each item grants a permission `level` to a single `user_name`, `group_name`, or `service_principal_name`. A principal cannot be set in both a resource's `permissions` and the top-level `permissions` mapping. + + See [\_](/dev-tools/bundles/settings.md#permissions) and [\_](/dev-tools/bundles/permissions.md). + "$fields": + "group_name": + "description": |- + The name of the group granted the permission level. + "level": + "description": |- + The permission level to apply. Cluster policies only support `CAN_USE`. + "service_principal_name": + "description": |- + The name of the service principal granted the permission level. + "user_name": + "description": |- + The name of the user granted the permission level. "clusters": "description": |- The cluster definitions for the bundle, where each key is the name of a cluster. diff --git a/bundle/schema/jsonschema.json b/bundle/schema/jsonschema.json index 8fdb1bfad83..425956ab06d 100644 --- a/bundle/schema/jsonschema.json +++ b/bundle/schema/jsonschema.json @@ -624,6 +624,11 @@ "description": "Cluster Policy name requested by the user. This has to be unique. Length must be between 1 and 100\ncharacters.", "$ref": "#/$defs/string" }, + "permissions": { + "description": "The permissions to apply to this resource.", + "$ref": "#/$defs/slice/github.com/databricks/cli/bundle/config/resources.ClusterPolicyPermission", + "markdownDescription": "A Sequence of permissions to apply to this resource, where each item grants a permission `level` to a single `user_name`, `group_name`, or `service_principal_name`. A principal cannot be set in both a resource's `permissions` and the top-level `permissions` mapping.\n\nSee [permissions](https://docs.databricks.com/dev-tools/bundles/settings.html#permissions) and [link](https://docs.databricks.com/dev-tools/bundles/permissions.html)." + }, "policy_family_definition_overrides": { "description": "Policy definition JSON document expressed in [Databricks Policy Definition Language](https://docs.databricks.com/administration-guide/clusters/policy-definition.html).\nThe JSON document must be passed as a string and cannot be embedded in the requests.\n\nYou can use this to customize the policy definition inherited from the policy family.\nPolicy rules specified here are merged into the inherited policy definition.", "$ref": "#/$defs/interface" @@ -641,6 +646,39 @@ } ] }, + "resources.ClusterPolicyPermission": { + "oneOf": [ + { + "type": "object", + "properties": { + "group_name": { + "description": "The name of the group granted the permission level.", + "$ref": "#/$defs/string" + }, + "level": { + "description": "The permission level to apply. Cluster policies only support `CAN_USE`.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/compute.ClusterPolicyPermissionLevel" + }, + "service_principal_name": { + "description": "The name of the service principal granted the permission level.", + "$ref": "#/$defs/string" + }, + "user_name": { + "description": "The name of the user granted the permission level.", + "$ref": "#/$defs/string" + } + }, + "additionalProperties": false, + "required": [ + "level" + ] + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, "resources.Dashboard": { "oneOf": [ { @@ -5513,6 +5551,21 @@ } ] }, + "compute.ClusterPolicyPermissionLevel": { + "oneOf": [ + { + "type": "string", + "description": "Permission level", + "enum": [ + "CAN_USE" + ] + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, "compute.ClusterSpec": { "oneOf": [ { @@ -15633,6 +15686,20 @@ } ] }, + "resources.ClusterPolicyPermission": { + "oneOf": [ + { + "type": "array", + "items": { + "$ref": "#/$defs/github.com/databricks/cli/bundle/config/resources.ClusterPolicyPermission" + } + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, "resources.InstancePoolPermission": { "oneOf": [ { From 8292b971d66a856917ce26ee3326b25b1888b60e Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Tue, 18 Aug 2026 10:42:31 +0000 Subject: [PATCH 02/12] Cover cluster_policies permissions with acceptance tests Add basic and out_of_band_change acceptance tests for the cluster_policies `permissions` field, filtering permission requests with print_requests.py. The basic test surfaced that FixPermissions injects the current user as CAN_MANAGE onto every resource with a permissions field. Cluster policies only accept CAN_USE, so the real backend would reject that. Add cluster_policies to fix_permissions.go's ignoredResources (like secret_scopes) so only the user-specified CAN_USE grants are sent. Co-authored-by: Isaac --- .../permissions/basic/databricks.yml | 13 +++ .../permissions/basic/out.test.toml | 2 + .../permissions/basic/output.txt | 84 +++++++++++++++++++ .../cluster_policies/permissions/basic/script | 22 +++++ .../permissions/basic/test.toml | 1 + .../out_of_band_change/databricks.yml | 11 +++ .../out_of_band_change/out.test.toml | 2 + .../permissions/out_of_band_change/output.txt | 78 +++++++++++++++++ .../permissions/out_of_band_change/script | 33 ++++++++ .../permissions/out_of_band_change/test.toml | 1 + .../resourcemutator/fix_permissions.go | 3 + 11 files changed, 250 insertions(+) create mode 100644 acceptance/bundle/resources/cluster_policies/permissions/basic/databricks.yml create mode 100644 acceptance/bundle/resources/cluster_policies/permissions/basic/out.test.toml create mode 100644 acceptance/bundle/resources/cluster_policies/permissions/basic/output.txt create mode 100644 acceptance/bundle/resources/cluster_policies/permissions/basic/script create mode 100644 acceptance/bundle/resources/cluster_policies/permissions/basic/test.toml create mode 100644 acceptance/bundle/resources/cluster_policies/permissions/out_of_band_change/databricks.yml create mode 100644 acceptance/bundle/resources/cluster_policies/permissions/out_of_band_change/out.test.toml create mode 100644 acceptance/bundle/resources/cluster_policies/permissions/out_of_band_change/output.txt create mode 100644 acceptance/bundle/resources/cluster_policies/permissions/out_of_band_change/script create mode 100644 acceptance/bundle/resources/cluster_policies/permissions/out_of_band_change/test.toml diff --git a/acceptance/bundle/resources/cluster_policies/permissions/basic/databricks.yml b/acceptance/bundle/resources/cluster_policies/permissions/basic/databricks.yml new file mode 100644 index 00000000000..7fd70916477 --- /dev/null +++ b/acceptance/bundle/resources/cluster_policies/permissions/basic/databricks.yml @@ -0,0 +1,13 @@ +bundle: + name: test_cluster_policy_permissions + +resources: + cluster_policies: + test_cluster_policy: + name: my_cluster_policy + definition: '{"spark_version":{"type":"fixed","value":"13.3.x-scala2.12"}}' + permissions: + - level: CAN_USE + group_name: users + - level: CAN_USE # TO_REMOVE + user_name: viewer@example.com # TO_REMOVE diff --git a/acceptance/bundle/resources/cluster_policies/permissions/basic/out.test.toml b/acceptance/bundle/resources/cluster_policies/permissions/basic/out.test.toml new file mode 100644 index 00000000000..0938e678987 --- /dev/null +++ b/acceptance/bundle/resources/cluster_policies/permissions/basic/out.test.toml @@ -0,0 +1,2 @@ +Cloud = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/bundle/resources/cluster_policies/permissions/basic/output.txt b/acceptance/bundle/resources/cluster_policies/permissions/basic/output.txt new file mode 100644 index 00000000000..b84045576cf --- /dev/null +++ b/acceptance/bundle/resources/cluster_policies/permissions/basic/output.txt @@ -0,0 +1,84 @@ + +=== Validate: permissions parse onto the cluster policy +>>> [CLI] bundle validate -o json +[ + { + "group_name": "users", + "level": "CAN_USE" + }, + { + "level": "CAN_USE", + "user_name": "viewer@example.com" + } +] + +=== Deploy: policy created and CAN_USE grants applied +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test_cluster_policy_permissions/default/files... +Created cluster_policies.test_cluster_policy +Created cluster_policies.test_cluster_policy.permissions +Files: 5 uploaded, 0 deleted +Resources: 2 created, 0 changed, 0 deleted, 0 unchanged + +>>> print_requests.py //permissions/cluster-policies +{ + "method": "PUT", + "path": "/api/2.0/permissions/cluster-policies/[UUID]", + "body": { + "access_control_list": [ + { + "group_name": "users", + "permission_level": "CAN_USE" + }, + { + "permission_level": "CAN_USE", + "user_name": "viewer@example.com" + } + ] + } +} + +>>> [CLI] bundle summary +Name: test_cluster_policy_permissions +Target: default +Workspace: + User: [USERNAME] + Path: /Workspace/Users/[USERNAME]/.bundle/test_cluster_policy_permissions/default +Resources: + Cluster Policies: + test_cluster_policy: + Name: my_cluster_policy + URL: [DATABRICKS_URL]/compute/policies/[UUID]?w=[NUMID] + +=== Plan is a no-op immediately after deploy +>>> [CLI] bundle plan +Plan: 0 to add, 0 to change, 0 to delete, 2 unchanged + +=== Remove one grant and redeploy: the ACL is set again without it +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test_cluster_policy_permissions/default/files... +Updated cluster_policies.test_cluster_policy.permissions +Files: 3 uploaded, 0 deleted +Resources: 0 created, 1 changed, 0 deleted, 1 unchanged + +>>> print_requests.py //permissions/cluster-policies +{ + "method": "PUT", + "path": "/api/2.0/permissions/cluster-policies/[UUID]", + "body": { + "access_control_list": [ + { + "group_name": "users", + "permission_level": "CAN_USE" + } + ] + } +} + +>>> [CLI] bundle destroy --auto-approve +The following resources will be deleted: + delete resources.cluster_policies.test_cluster_policy + +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test_cluster_policy_permissions/default + +Destroy: 1 deleted diff --git a/acceptance/bundle/resources/cluster_policies/permissions/basic/script b/acceptance/bundle/resources/cluster_policies/permissions/basic/script new file mode 100644 index 00000000000..6184d90781e --- /dev/null +++ b/acceptance/bundle/resources/cluster_policies/permissions/basic/script @@ -0,0 +1,22 @@ +cleanup() { + trace $CLI bundle destroy --auto-approve + rm -f out.requests.txt +} +trap cleanup EXIT + +title "Validate: permissions parse onto the cluster policy" +trace $CLI bundle validate -o json | jq ".resources.cluster_policies.test_cluster_policy.permissions" + +title "Deploy: policy created and CAN_USE grants applied" +trace $CLI bundle deploy +trace print_requests.py //permissions/cluster-policies + +trace $CLI bundle summary + +title "Plan is a no-op immediately after deploy" +trace $CLI bundle plan + +title "Remove one grant and redeploy: the ACL is set again without it" +grep -v TO_REMOVE databricks.yml > updated.yml && mv updated.yml databricks.yml +trace $CLI bundle deploy +trace print_requests.py //permissions/cluster-policies diff --git a/acceptance/bundle/resources/cluster_policies/permissions/basic/test.toml b/acceptance/bundle/resources/cluster_policies/permissions/basic/test.toml new file mode 100644 index 00000000000..cba9a272efc --- /dev/null +++ b/acceptance/bundle/resources/cluster_policies/permissions/basic/test.toml @@ -0,0 +1 @@ +Ignore = [".databricks", "databricks.yml", "updated.yml"] diff --git a/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_change/databricks.yml b/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_change/databricks.yml new file mode 100644 index 00000000000..0c151e08e51 --- /dev/null +++ b/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_change/databricks.yml @@ -0,0 +1,11 @@ +bundle: + name: test_cluster_policy_permissions_drift + +resources: + cluster_policies: + test_cluster_policy: + name: my_cluster_policy + definition: '{"spark_version":{"type":"fixed","value":"13.3.x-scala2.12"}}' + permissions: + - level: CAN_USE + group_name: users diff --git a/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_change/out.test.toml b/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_change/out.test.toml new file mode 100644 index 00000000000..0938e678987 --- /dev/null +++ b/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_change/out.test.toml @@ -0,0 +1,2 @@ +Cloud = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_change/output.txt b/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_change/output.txt new file mode 100644 index 00000000000..3fd663498dc --- /dev/null +++ b/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_change/output.txt @@ -0,0 +1,78 @@ + +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test_cluster_policy_permissions_drift/default/files... +Created cluster_policies.test_cluster_policy +Created cluster_policies.test_cluster_policy.permissions +Files: 5 uploaded, 0 deleted +Resources: 2 created, 0 changed, 0 deleted, 0 unchanged + +=== Plan is a no-op immediately after deploy +>>> [CLI] bundle plan +Plan: 0 to add, 0 to change, 0 to delete, 2 unchanged + +=== Grant an extra principal out of band +>>> [CLI] cluster-policies set-permissions [TEST_CLUSTER_POLICY_ID] --json {"access_control_list":[{"group_name":"users","permission_level":"CAN_USE"},{"user_name":"intruder@example.com","permission_level":"CAN_USE"}]} +{ + "access_control_list": [ + { + "all_permissions": [ + { + "inherited": false, + "permission_level": "CAN_USE" + } + ], + "group_name": "users" + }, + { + "all_permissions": [ + { + "inherited": false, + "permission_level": "CAN_USE" + } + ], + "display_name": "intruder@example.com", + "user_name": "intruder@example.com" + } + ], + "object_id": "/cluster-policies/[TEST_CLUSTER_POLICY_ID]", + "object_type": "cluster-policy" +} + +=== Plan detects the permission drift +>>> [CLI] bundle plan +update cluster_policies.test_cluster_policy.permissions + +Plan: 0 to add, 1 to change, 0 to delete, 1 unchanged + +=== Redeploy reconciles the ACL back to the configured grants +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test_cluster_policy_permissions_drift/default/files... +Updated cluster_policies.test_cluster_policy.permissions +Files: 2 uploaded, 0 deleted +Resources: 0 created, 1 changed, 0 deleted, 1 unchanged + +>>> print_requests.py //permissions/cluster-policies +{ + "method": "PUT", + "path": "/api/2.0/permissions/cluster-policies/[TEST_CLUSTER_POLICY_ID]", + "body": { + "access_control_list": [ + { + "group_name": "users", + "permission_level": "CAN_USE" + } + ] + } +} + +=== Plan is a no-op again +>>> [CLI] bundle plan +Plan: 0 to add, 0 to change, 0 to delete, 2 unchanged + +>>> [CLI] bundle destroy --auto-approve +The following resources will be deleted: + delete resources.cluster_policies.test_cluster_policy + +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test_cluster_policy_permissions_drift/default + +Destroy: 1 deleted diff --git a/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_change/script b/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_change/script new file mode 100644 index 00000000000..fad8a31a138 --- /dev/null +++ b/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_change/script @@ -0,0 +1,33 @@ +cleanup() { + trace $CLI bundle destroy --auto-approve + rm -f out.requests.txt +} +trap cleanup EXIT + +trace $CLI bundle deploy + +title "Plan is a no-op immediately after deploy" +trace $CLI bundle plan + +policy_id="$(read_id.py test_cluster_policy)" + +# Simulate an out-of-band ACL change the way an admin would in the UI: grant an +# extra principal directly through the permissions API without touching +# databricks.yml. The recorded bundle state is now stale, so the next plan must +# detect the drift. +title "Grant an extra principal out of band" +trace $CLI cluster-policies set-permissions "$policy_id" --json '{"access_control_list":[{"group_name":"users","permission_level":"CAN_USE"},{"user_name":"intruder@example.com","permission_level":"CAN_USE"}]}' + +# Discard the out-of-band request so the verification below captures only the +# reconciling Set issued by the redeploy. +rm -f out.requests.txt + +title "Plan detects the permission drift" +trace $CLI bundle plan + +title "Redeploy reconciles the ACL back to the configured grants" +trace $CLI bundle deploy +trace print_requests.py //permissions/cluster-policies + +title "Plan is a no-op again" +trace $CLI bundle plan diff --git a/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_change/test.toml b/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_change/test.toml new file mode 100644 index 00000000000..7ccf95a8a6a --- /dev/null +++ b/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_change/test.toml @@ -0,0 +1 @@ +Ignore = [".databricks", "databricks.yml"] diff --git a/bundle/config/mutator/resourcemutator/fix_permissions.go b/bundle/config/mutator/resourcemutator/fix_permissions.go index 6d8a44bd800..9e6c785b0d9 100644 --- a/bundle/config/mutator/resourcemutator/fix_permissions.go +++ b/bundle/config/mutator/resourcemutator/fix_permissions.go @@ -25,6 +25,9 @@ var hasIsOwner = map[string]bool{ var ignoredResources = map[string]bool{ "secret_scopes": true, + // Cluster policies only support CAN_USE; injecting the current user as + // CAN_MANAGE/IS_OWNER would be rejected by the permissions API. + "cluster_policies": true, } // When processing permissions, we need to implement these constraints: From 7d7bf4e55b26353e7cc777cdbe9b81ca9162aaaf Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Tue, 18 Aug 2026 10:49:47 +0000 Subject: [PATCH 03/12] Assert cluster_policies permission levels on cloud Add a Cloud=true test that deploys a policy with CAN_USE (accepted) and then asserts a direct CAN_MANAGE set-permissions call is rejected. Model the rejection in the testserver: the permissions API only accepts CAN_USE for cluster policies, so a non-CAN_USE level returns 400. This keeps the local run and the cloud run in agreement. The backend error text differs between the fake and a real workspace, so it is routed to a LOG file and musterr asserts only that the request fails. Co-authored-by: Isaac --- .../permissions/levels/databricks.yml.tmpl | 11 +++++++ .../permissions/levels/out.test.toml | 2 ++ .../permissions/levels/output.txt | 30 +++++++++++++++++++ .../permissions/levels/script | 19 ++++++++++++ .../permissions/levels/test.toml | 2 ++ libs/testserver/permissions.go | 17 +++++++++++ 6 files changed, 81 insertions(+) create mode 100644 acceptance/bundle/resources/cluster_policies/permissions/levels/databricks.yml.tmpl create mode 100644 acceptance/bundle/resources/cluster_policies/permissions/levels/out.test.toml create mode 100644 acceptance/bundle/resources/cluster_policies/permissions/levels/output.txt create mode 100644 acceptance/bundle/resources/cluster_policies/permissions/levels/script create mode 100644 acceptance/bundle/resources/cluster_policies/permissions/levels/test.toml diff --git a/acceptance/bundle/resources/cluster_policies/permissions/levels/databricks.yml.tmpl b/acceptance/bundle/resources/cluster_policies/permissions/levels/databricks.yml.tmpl new file mode 100644 index 00000000000..8ebdcb8c366 --- /dev/null +++ b/acceptance/bundle/resources/cluster_policies/permissions/levels/databricks.yml.tmpl @@ -0,0 +1,11 @@ +bundle: + name: cluster-policy-permission-levels-$UNIQUE_NAME + +resources: + cluster_policies: + test_cluster_policy: + name: my_cluster_policy-$UNIQUE_NAME + definition: '{"spark_version":{"type":"fixed","value":"13.3.x-scala2.12"}}' + permissions: + - level: CAN_USE + group_name: users diff --git a/acceptance/bundle/resources/cluster_policies/permissions/levels/out.test.toml b/acceptance/bundle/resources/cluster_policies/permissions/levels/out.test.toml new file mode 100644 index 00000000000..c502b28221b --- /dev/null +++ b/acceptance/bundle/resources/cluster_policies/permissions/levels/out.test.toml @@ -0,0 +1,2 @@ +Cloud = true +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/bundle/resources/cluster_policies/permissions/levels/output.txt b/acceptance/bundle/resources/cluster_policies/permissions/levels/output.txt new file mode 100644 index 00000000000..b6a65434bff --- /dev/null +++ b/acceptance/bundle/resources/cluster_policies/permissions/levels/output.txt @@ -0,0 +1,30 @@ + +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/cluster-policy-permission-levels-[UNIQUE_NAME]/default/files... +Created cluster_policies.test_cluster_policy +Created cluster_policies.test_cluster_policy.permissions +Files: 6 uploaded, 0 deleted +Resources: 2 created, 0 changed, 0 deleted, 0 unchanged + +>>> print_requests.py //permissions/cluster-policies +{ + "method": "PUT", + "path": "/api/2.0/permissions/cluster-policies/[TEST_CLUSTER_POLICY_ID]", + "body": { + "access_control_list": [ + { + "group_name": "users", + "permission_level": "CAN_USE" + } + ] + } +} + +=== CAN_MANAGE is rejected +>>> [CLI] bundle destroy --auto-approve +The following resources will be deleted: + delete resources.cluster_policies.test_cluster_policy + +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/cluster-policy-permission-levels-[UNIQUE_NAME]/default + +Destroy: 1 deleted diff --git a/acceptance/bundle/resources/cluster_policies/permissions/levels/script b/acceptance/bundle/resources/cluster_policies/permissions/levels/script new file mode 100644 index 00000000000..f5f2b3e6a27 --- /dev/null +++ b/acceptance/bundle/resources/cluster_policies/permissions/levels/script @@ -0,0 +1,19 @@ +cleanup() { + trace $CLI bundle destroy --auto-approve + rm -f out.requests.txt +} +trap cleanup EXIT + +envsubst < databricks.yml.tmpl > databricks.yml + +# CAN_USE is the only level cluster policies accept: the deploy succeeds and the +# permissions PUT carries CAN_USE. +trace $CLI bundle deploy +policy_id="$(read_id.py test_cluster_policy)" +trace print_requests.py //permissions/cluster-policies + +# CAN_MANAGE is rejected by the permissions API. The backend error text differs +# between the fake server and a real workspace, so it is routed to a LOG file; +# musterr asserts only that the request fails. +title "CAN_MANAGE is rejected" +musterr $CLI cluster-policies set-permissions "$policy_id" --json '{"access_control_list":[{"group_name":"users","permission_level":"CAN_MANAGE"}]}' &> LOG.can_manage diff --git a/acceptance/bundle/resources/cluster_policies/permissions/levels/test.toml b/acceptance/bundle/resources/cluster_policies/permissions/levels/test.toml new file mode 100644 index 00000000000..3dd59d27d6c --- /dev/null +++ b/acceptance/bundle/resources/cluster_policies/permissions/levels/test.toml @@ -0,0 +1,2 @@ +Cloud = true +Ignore = [".databricks", "databricks.yml"] diff --git a/libs/testserver/permissions.go b/libs/testserver/permissions.go index 61f9907ae2e..24ae7a8a4b2 100644 --- a/libs/testserver/permissions.go +++ b/libs/testserver/permissions.go @@ -238,6 +238,23 @@ func (s *FakeWorkspace) SetPermissions(req Request) any { } } + // Cluster policies only support CAN_USE; the real permissions API rejects any + // other level. Model that so a deploy or a set-permissions call requesting + // CAN_MANAGE fails the same way locally and on cloud. + if requestObjectType == "cluster-policies" { + for _, acl := range updateRequest.AccessControlList { + if acl.PermissionLevel != "" && acl.PermissionLevel != "CAN_USE" { + return Response{ + StatusCode: 400, + Body: map[string]string{ + "error_code": "INVALID_PARAMETER_VALUE", + "message": fmt.Sprintf("Cluster policy permissions only support CAN_USE, got %s", acl.PermissionLevel), + }, + } + } + } + } + responseObjectID := fmt.Sprintf("/%s/%s", requestObjectType, objectId) // Get existing permissions or create new ones From 107f37aaf4af66edf24ae041d38855c0d7e4fd7d Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Tue, 18 Aug 2026 10:52:48 +0000 Subject: [PATCH 04/12] Regenerate validation and refschema for cluster_policies permissions Runs generate-check output: the refschema field list, the level enum (CAN_USE only), and the required-field entry for the new cluster_policies permissions block. Co-authored-by: Isaac --- acceptance/bundle/refschema/out.fields.txt | 6 ++++++ bundle/internal/validation/generated/enum_fields.go | 2 ++ bundle/internal/validation/generated/required_fields.go | 1 + 3 files changed, 9 insertions(+) diff --git a/acceptance/bundle/refschema/out.fields.txt b/acceptance/bundle/refschema/out.fields.txt index ee09e0e9ba0..de252dc4162 100644 --- a/acceptance/bundle/refschema/out.fields.txt +++ b/acceptance/bundle/refschema/out.fields.txt @@ -333,6 +333,12 @@ resources.cluster_policies.*.policy_family_definition_overrides string ALL resources.cluster_policies.*.policy_family_id string ALL resources.cluster_policies.*.policy_id string REMOTE resources.cluster_policies.*.url string INPUT +resources.cluster_policies.*.permissions.object_id string ALL +resources.cluster_policies.*.permissions[*] dresources.StatePermission ALL +resources.cluster_policies.*.permissions[*].group_name string ALL +resources.cluster_policies.*.permissions[*].level iam.PermissionLevel ALL +resources.cluster_policies.*.permissions[*].service_principal_name string ALL +resources.cluster_policies.*.permissions[*].user_name string ALL resources.clusters.*.apply_policy_default_values bool ALL resources.clusters.*.autoscale *compute.AutoScale ALL resources.clusters.*.autoscale.max_workers int ALL diff --git a/bundle/internal/validation/generated/enum_fields.go b/bundle/internal/validation/generated/enum_fields.go index 1abda54bfa7..5e62e921687 100644 --- a/bundle/internal/validation/generated/enum_fields.go +++ b/bundle/internal/validation/generated/enum_fields.go @@ -41,6 +41,8 @@ var EnumFields = map[string][]string{ "resources.catalogs.*.grants[*].privileges[*]": {"ACCESS", "ALL_PRIVILEGES", "APPLY_TAG", "BROWSE", "CREATE", "CREATE_CATALOG", "CREATE_CLEAN_ROOM", "CREATE_CONNECTION", "CREATE_EXTERNAL_LOCATION", "CREATE_EXTERNAL_TABLE", "CREATE_EXTERNAL_VOLUME", "CREATE_FOREIGN_CATALOG", "CREATE_FOREIGN_SECURABLE", "CREATE_FUNCTION", "CREATE_MANAGED_STORAGE", "CREATE_MATERIALIZED_VIEW", "CREATE_MODEL", "CREATE_PROVIDER", "CREATE_RECIPIENT", "CREATE_SCHEMA", "CREATE_SERVICE_CREDENTIAL", "CREATE_SHARE", "CREATE_STORAGE_CREDENTIAL", "CREATE_TABLE", "CREATE_VIEW", "CREATE_VOLUME", "EXECUTE", "EXECUTE_CLEAN_ROOM_TASK", "EXTERNAL_USE_SCHEMA", "MANAGE", "MANAGE_ALLOWLIST", "MODIFY", "MODIFY_CLEAN_ROOM", "READ_FILES", "READ_METADATA", "READ_PRIVATE_FILES", "READ_VOLUME", "REFRESH", "SELECT", "SET_SHARE_PERMISSION", "USAGE", "USE_CATALOG", "USE_CONNECTION", "USE_MARKETPLACE_ASSETS", "USE_PROVIDER", "USE_RECIPIENT", "USE_SCHEMA", "USE_SHARE", "WRITE_FILES", "WRITE_PRIVATE_FILES", "WRITE_VOLUME"}, + "resources.cluster_policies.*.permissions[*].level": {"CAN_USE"}, + "resources.clusters.*.aws_attributes.availability": {"ON_DEMAND", "SPOT", "SPOT_WITH_FALLBACK"}, "resources.clusters.*.aws_attributes.ebs_volume_type": {"GENERAL_PURPOSE_SSD", "THROUGHPUT_OPTIMIZED_HDD"}, "resources.clusters.*.azure_attributes.availability": {"ON_DEMAND_AZURE", "SPOT_AZURE", "SPOT_WITH_FALLBACK_AZURE"}, diff --git a/bundle/internal/validation/generated/required_fields.go b/bundle/internal/validation/generated/required_fields.go index 26de25e74db..617f5a65ecc 100644 --- a/bundle/internal/validation/generated/required_fields.go +++ b/bundle/internal/validation/generated/required_fields.go @@ -44,6 +44,7 @@ var RequiredFields = map[string][]string{ "resources.cluster_policies.*.libraries[*].cran": {"package"}, "resources.cluster_policies.*.libraries[*].maven": {"coordinates"}, "resources.cluster_policies.*.libraries[*].pypi": {"package"}, + "resources.cluster_policies.*.permissions[*]": {"level"}, "resources.clusters.*.cluster_log_conf.dbfs": {"destination"}, "resources.clusters.*.cluster_log_conf.s3": {"destination"}, From 415690fe6aa7e70819d080dd4381dde99cefe4cb Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Tue, 18 Aug 2026 11:34:14 +0000 Subject: [PATCH 05/12] Cover cluster_policies permissions in invariant configs TestInvariantConfigsCoverage requires every resource with a permissions field to have an invariant config exercising it. Attach a CAN_USE grant to the cluster_policies invariant config. Co-authored-by: Isaac --- acceptance/bundle/invariant/configs/cluster_policy.yml.tmpl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/acceptance/bundle/invariant/configs/cluster_policy.yml.tmpl b/acceptance/bundle/invariant/configs/cluster_policy.yml.tmpl index aa514b0e0db..35c1dd4a02f 100644 --- a/acceptance/bundle/invariant/configs/cluster_policy.yml.tmpl +++ b/acceptance/bundle/invariant/configs/cluster_policy.yml.tmpl @@ -6,3 +6,6 @@ resources: foo: name: test-cluster-policy-$UNIQUE_NAME definition: '{"spark_version":{"type":"fixed","value":"13.3.x-scala2.12"}}' + permissions: + - level: CAN_USE + group_name: users From 20df67a49a7c9aa13e75003d4f5fab8f1f718610 Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Mon, 24 Aug 2026 14:36:57 +0000 Subject: [PATCH 06/12] Drive cluster_policies CAN_MANAGE rejection through bundle deploy Address review feedback: instead of a raw set-permissions CLI call, flip the grant to CAN_MANAGE with update_file.py and assert the bundle deploy fails. Sync the testserver's rejection message to the real permissions API ("Unknown Cluster Policy Permission Level: ", captured from a cloud run) so the fake and cloud agree, and assert it verbatim in the golden. Verified against a real workspace via deco env run. Co-authored-by: Isaac --- .../permissions/levels/output.txt | 6 ++++++ .../cluster_policies/permissions/levels/script | 15 +++++++-------- libs/testserver/permissions.go | 7 +++---- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/acceptance/bundle/resources/cluster_policies/permissions/levels/output.txt b/acceptance/bundle/resources/cluster_policies/permissions/levels/output.txt index b6a65434bff..000b93ac0a2 100644 --- a/acceptance/bundle/resources/cluster_policies/permissions/levels/output.txt +++ b/acceptance/bundle/resources/cluster_policies/permissions/levels/output.txt @@ -21,7 +21,13 @@ Resources: 2 created, 0 changed, 0 deleted, 0 unchanged } === CAN_MANAGE is rejected +Error: cannot update resources.cluster_policies.test_cluster_policy.permissions: updating id=/cluster-policies/[TEST_CLUSTER_POLICY_ID]: Unknown Cluster Policy Permission Level: CAN_MANAGE (400 INVALID_PARAMETER_VALUE) + >>> [CLI] bundle destroy --auto-approve +Warning: invalid value "CAN_MANAGE" for enum field. Valid values are [CAN_USE] + at resources.cluster_policies.test_cluster_policy.permissions[0].level + in databricks.yml:10:18 + The following resources will be deleted: delete resources.cluster_policies.test_cluster_policy diff --git a/acceptance/bundle/resources/cluster_policies/permissions/levels/script b/acceptance/bundle/resources/cluster_policies/permissions/levels/script index f5f2b3e6a27..ff20afa28e0 100644 --- a/acceptance/bundle/resources/cluster_policies/permissions/levels/script +++ b/acceptance/bundle/resources/cluster_policies/permissions/levels/script @@ -6,14 +6,13 @@ trap cleanup EXIT envsubst < databricks.yml.tmpl > databricks.yml -# CAN_USE is the only level cluster policies accept: the deploy succeeds and the -# permissions PUT carries CAN_USE. +# CAN_USE is accepted: deploy succeeds and the PUT carries CAN_USE. trace $CLI bundle deploy -policy_id="$(read_id.py test_cluster_policy)" +read_id.py test_cluster_policy > /dev/null trace print_requests.py //permissions/cluster-policies -# CAN_MANAGE is rejected by the permissions API. The backend error text differs -# between the fake server and a real workspace, so it is routed to a LOG file; -# musterr asserts only that the request fails. -title "CAN_MANAGE is rejected" -musterr $CLI cluster-policies set-permissions "$policy_id" --json '{"access_control_list":[{"group_name":"users","permission_level":"CAN_MANAGE"}]}' &> LOG.can_manage +# CAN_MANAGE is rejected, so the redeploy fails with the backend's error. +title "CAN_MANAGE is rejected\n" +update_file.py databricks.yml CAN_USE CAN_MANAGE +musterr $CLI bundle deploy > LOG.deploy 2>&1 +grep "Error: cannot update" LOG.deploy diff --git a/libs/testserver/permissions.go b/libs/testserver/permissions.go index 24ae7a8a4b2..a3584c1d49a 100644 --- a/libs/testserver/permissions.go +++ b/libs/testserver/permissions.go @@ -238,9 +238,8 @@ func (s *FakeWorkspace) SetPermissions(req Request) any { } } - // Cluster policies only support CAN_USE; the real permissions API rejects any - // other level. Model that so a deploy or a set-permissions call requesting - // CAN_MANAGE fails the same way locally and on cloud. + // Cluster policies only support CAN_USE; the real API rejects other levels + // with this exact message. Model it so deploys fail the same way on cloud. if requestObjectType == "cluster-policies" { for _, acl := range updateRequest.AccessControlList { if acl.PermissionLevel != "" && acl.PermissionLevel != "CAN_USE" { @@ -248,7 +247,7 @@ func (s *FakeWorkspace) SetPermissions(req Request) any { StatusCode: 400, Body: map[string]string{ "error_code": "INVALID_PARAMETER_VALUE", - "message": fmt.Sprintf("Cluster policy permissions only support CAN_USE, got %s", acl.PermissionLevel), + "message": fmt.Sprintf("Unknown Cluster Policy Permission Level: %s", acl.PermissionLevel), }, } } From c21283b2887c473891fa5b14bb5055c3343ef55c Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Mon, 24 Aug 2026 14:55:34 +0000 Subject: [PATCH 07/12] Assert the CAN_MANAGE rejection message via LOG + contains.py The failing redeploy's upload count differs between the fake and cloud, so its output cannot live in the golden. Follow the repo convention: capture the deploy to LOG and assert the backend's error message with contains.py. Verified against a real workspace via deco env run. Co-authored-by: Isaac --- .../cluster_policies/permissions/levels/output.txt | 2 -- .../resources/cluster_policies/permissions/levels/script | 8 +++++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/acceptance/bundle/resources/cluster_policies/permissions/levels/output.txt b/acceptance/bundle/resources/cluster_policies/permissions/levels/output.txt index 000b93ac0a2..40fccf6448b 100644 --- a/acceptance/bundle/resources/cluster_policies/permissions/levels/output.txt +++ b/acceptance/bundle/resources/cluster_policies/permissions/levels/output.txt @@ -21,8 +21,6 @@ Resources: 2 created, 0 changed, 0 deleted, 0 unchanged } === CAN_MANAGE is rejected -Error: cannot update resources.cluster_policies.test_cluster_policy.permissions: updating id=/cluster-policies/[TEST_CLUSTER_POLICY_ID]: Unknown Cluster Policy Permission Level: CAN_MANAGE (400 INVALID_PARAMETER_VALUE) - >>> [CLI] bundle destroy --auto-approve Warning: invalid value "CAN_MANAGE" for enum field. Valid values are [CAN_USE] at resources.cluster_policies.test_cluster_policy.permissions[0].level diff --git a/acceptance/bundle/resources/cluster_policies/permissions/levels/script b/acceptance/bundle/resources/cluster_policies/permissions/levels/script index ff20afa28e0..bb5762483b1 100644 --- a/acceptance/bundle/resources/cluster_policies/permissions/levels/script +++ b/acceptance/bundle/resources/cluster_policies/permissions/levels/script @@ -12,7 +12,9 @@ read_id.py test_cluster_policy > /dev/null trace print_requests.py //permissions/cluster-policies # CAN_MANAGE is rejected, so the redeploy fails with the backend's error. -title "CAN_MANAGE is rejected\n" +# Output goes to LOG (upload counts differ between the fake and cloud); the +# error message is deterministic and asserted here. +title "CAN_MANAGE is rejected" update_file.py databricks.yml CAN_USE CAN_MANAGE -musterr $CLI bundle deploy > LOG.deploy 2>&1 -grep "Error: cannot update" LOG.deploy +musterr $CLI bundle deploy &> LOG.deploy +cat LOG.deploy | contains.py "Unknown Cluster Policy Permission Level: CAN_MANAGE" > /dev/null From 99286307a29d18fea003b66878d0ae4fb00a2bcd Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Mon, 24 Aug 2026 15:38:34 +0000 Subject: [PATCH 08/12] Keep permission acceptance tests local after base defaulted Cloud=true The base branch now sets Cloud=true for cluster_policies tests. The basic and out_of_band_change permission tests use a fixed policy name and are not cloud-safe for concurrent runs, so pin them Cloud=false. The levels test remains the cloud-run coverage (it uses UNIQUE_NAME). Co-authored-by: Isaac --- .../resources/cluster_policies/permissions/basic/test.toml | 4 ++++ .../cluster_policies/permissions/out_of_band_change/test.toml | 3 +++ 2 files changed, 7 insertions(+) diff --git a/acceptance/bundle/resources/cluster_policies/permissions/basic/test.toml b/acceptance/bundle/resources/cluster_policies/permissions/basic/test.toml index cba9a272efc..c03a6d63274 100644 --- a/acceptance/bundle/resources/cluster_policies/permissions/basic/test.toml +++ b/acceptance/bundle/resources/cluster_policies/permissions/basic/test.toml @@ -1 +1,5 @@ +# Local-only: this deterministic test uses a fixed policy name and does not mask +# the server id, so it is not safe for concurrent cloud runs. Cloud coverage of +# the permission behavior lives in the levels test. +Cloud = false Ignore = [".databricks", "databricks.yml", "updated.yml"] diff --git a/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_change/test.toml b/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_change/test.toml index 7ccf95a8a6a..4a3471ddc43 100644 --- a/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_change/test.toml +++ b/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_change/test.toml @@ -1 +1,4 @@ +# Local-only: fixed policy name is not safe for concurrent cloud runs. Cloud +# coverage of the permission behavior lives in the levels test. +Cloud = false Ignore = [".databricks", "databricks.yml"] From 8ccd0923e4eccc5b9bda44c4455a2d655b7d69b7 Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Mon, 24 Aug 2026 15:56:43 +0000 Subject: [PATCH 09/12] Rename permission drift test to out_of_band_grant More precise than out_of_band_change (which, at the resource level, means a definition change): this test grants an extra principal out of band and checks the bundle reconciles it. Parallels the out_of_band_deletion naming from the genie_spaces precedent. Co-authored-by: Isaac --- .../{out_of_band_change => out_of_band_grant}/databricks.yml | 0 .../{out_of_band_change => out_of_band_grant}/out.test.toml | 0 .../{out_of_band_change => out_of_band_grant}/output.txt | 0 .../permissions/{out_of_band_change => out_of_band_grant}/script | 0 .../{out_of_band_change => out_of_band_grant}/test.toml | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename acceptance/bundle/resources/cluster_policies/permissions/{out_of_band_change => out_of_band_grant}/databricks.yml (100%) rename acceptance/bundle/resources/cluster_policies/permissions/{out_of_band_change => out_of_band_grant}/out.test.toml (100%) rename acceptance/bundle/resources/cluster_policies/permissions/{out_of_band_change => out_of_band_grant}/output.txt (100%) rename acceptance/bundle/resources/cluster_policies/permissions/{out_of_band_change => out_of_band_grant}/script (100%) rename acceptance/bundle/resources/cluster_policies/permissions/{out_of_band_change => out_of_band_grant}/test.toml (100%) diff --git a/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_change/databricks.yml b/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_grant/databricks.yml similarity index 100% rename from acceptance/bundle/resources/cluster_policies/permissions/out_of_band_change/databricks.yml rename to acceptance/bundle/resources/cluster_policies/permissions/out_of_band_grant/databricks.yml diff --git a/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_change/out.test.toml b/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_grant/out.test.toml similarity index 100% rename from acceptance/bundle/resources/cluster_policies/permissions/out_of_band_change/out.test.toml rename to acceptance/bundle/resources/cluster_policies/permissions/out_of_band_grant/out.test.toml diff --git a/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_change/output.txt b/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_grant/output.txt similarity index 100% rename from acceptance/bundle/resources/cluster_policies/permissions/out_of_band_change/output.txt rename to acceptance/bundle/resources/cluster_policies/permissions/out_of_band_grant/output.txt diff --git a/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_change/script b/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_grant/script similarity index 100% rename from acceptance/bundle/resources/cluster_policies/permissions/out_of_band_change/script rename to acceptance/bundle/resources/cluster_policies/permissions/out_of_band_grant/script diff --git a/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_change/test.toml b/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_grant/test.toml similarity index 100% rename from acceptance/bundle/resources/cluster_policies/permissions/out_of_band_change/test.toml rename to acceptance/bundle/resources/cluster_policies/permissions/out_of_band_grant/test.toml From 4c3cd959d7a12cb744d5cb16dd0e9f0e3d3266a8 Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Mon, 24 Aug 2026 15:59:33 +0000 Subject: [PATCH 10/12] Add out_of_band_deletion test; remove redundant basic test Add v1 out-of-band deletion coverage for cluster_policies permissions: the policy is deleted out of band, its ACL lingers (no 404, unlike a v2 cascade delete), and plan recreates it. Modeled on the genie_spaces out_of_band_deletion precedent. Remove the basic test: its create/deploy path is already covered by the invariant config, per review feedback. Co-authored-by: Isaac --- .../permissions/basic/output.txt | 84 ------------------- .../cluster_policies/permissions/basic/script | 22 ----- .../permissions/basic/test.toml | 5 -- .../databricks.yml | 4 +- .../out.test.toml | 0 .../out_of_band_deletion/output.txt | 58 +++++++++++++ .../permissions/out_of_band_deletion/script | 22 +++++ .../out_of_band_deletion/test.toml | 4 + 8 files changed, 85 insertions(+), 114 deletions(-) delete mode 100644 acceptance/bundle/resources/cluster_policies/permissions/basic/output.txt delete mode 100644 acceptance/bundle/resources/cluster_policies/permissions/basic/script delete mode 100644 acceptance/bundle/resources/cluster_policies/permissions/basic/test.toml rename acceptance/bundle/resources/cluster_policies/permissions/{basic => out_of_band_deletion}/databricks.yml (65%) rename acceptance/bundle/resources/cluster_policies/permissions/{basic => out_of_band_deletion}/out.test.toml (100%) create mode 100644 acceptance/bundle/resources/cluster_policies/permissions/out_of_band_deletion/output.txt create mode 100644 acceptance/bundle/resources/cluster_policies/permissions/out_of_band_deletion/script create mode 100644 acceptance/bundle/resources/cluster_policies/permissions/out_of_band_deletion/test.toml diff --git a/acceptance/bundle/resources/cluster_policies/permissions/basic/output.txt b/acceptance/bundle/resources/cluster_policies/permissions/basic/output.txt deleted file mode 100644 index b84045576cf..00000000000 --- a/acceptance/bundle/resources/cluster_policies/permissions/basic/output.txt +++ /dev/null @@ -1,84 +0,0 @@ - -=== Validate: permissions parse onto the cluster policy ->>> [CLI] bundle validate -o json -[ - { - "group_name": "users", - "level": "CAN_USE" - }, - { - "level": "CAN_USE", - "user_name": "viewer@example.com" - } -] - -=== Deploy: policy created and CAN_USE grants applied ->>> [CLI] bundle deploy -Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test_cluster_policy_permissions/default/files... -Created cluster_policies.test_cluster_policy -Created cluster_policies.test_cluster_policy.permissions -Files: 5 uploaded, 0 deleted -Resources: 2 created, 0 changed, 0 deleted, 0 unchanged - ->>> print_requests.py //permissions/cluster-policies -{ - "method": "PUT", - "path": "/api/2.0/permissions/cluster-policies/[UUID]", - "body": { - "access_control_list": [ - { - "group_name": "users", - "permission_level": "CAN_USE" - }, - { - "permission_level": "CAN_USE", - "user_name": "viewer@example.com" - } - ] - } -} - ->>> [CLI] bundle summary -Name: test_cluster_policy_permissions -Target: default -Workspace: - User: [USERNAME] - Path: /Workspace/Users/[USERNAME]/.bundle/test_cluster_policy_permissions/default -Resources: - Cluster Policies: - test_cluster_policy: - Name: my_cluster_policy - URL: [DATABRICKS_URL]/compute/policies/[UUID]?w=[NUMID] - -=== Plan is a no-op immediately after deploy ->>> [CLI] bundle plan -Plan: 0 to add, 0 to change, 0 to delete, 2 unchanged - -=== Remove one grant and redeploy: the ACL is set again without it ->>> [CLI] bundle deploy -Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test_cluster_policy_permissions/default/files... -Updated cluster_policies.test_cluster_policy.permissions -Files: 3 uploaded, 0 deleted -Resources: 0 created, 1 changed, 0 deleted, 1 unchanged - ->>> print_requests.py //permissions/cluster-policies -{ - "method": "PUT", - "path": "/api/2.0/permissions/cluster-policies/[UUID]", - "body": { - "access_control_list": [ - { - "group_name": "users", - "permission_level": "CAN_USE" - } - ] - } -} - ->>> [CLI] bundle destroy --auto-approve -The following resources will be deleted: - delete resources.cluster_policies.test_cluster_policy - -All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test_cluster_policy_permissions/default - -Destroy: 1 deleted diff --git a/acceptance/bundle/resources/cluster_policies/permissions/basic/script b/acceptance/bundle/resources/cluster_policies/permissions/basic/script deleted file mode 100644 index 6184d90781e..00000000000 --- a/acceptance/bundle/resources/cluster_policies/permissions/basic/script +++ /dev/null @@ -1,22 +0,0 @@ -cleanup() { - trace $CLI bundle destroy --auto-approve - rm -f out.requests.txt -} -trap cleanup EXIT - -title "Validate: permissions parse onto the cluster policy" -trace $CLI bundle validate -o json | jq ".resources.cluster_policies.test_cluster_policy.permissions" - -title "Deploy: policy created and CAN_USE grants applied" -trace $CLI bundle deploy -trace print_requests.py //permissions/cluster-policies - -trace $CLI bundle summary - -title "Plan is a no-op immediately after deploy" -trace $CLI bundle plan - -title "Remove one grant and redeploy: the ACL is set again without it" -grep -v TO_REMOVE databricks.yml > updated.yml && mv updated.yml databricks.yml -trace $CLI bundle deploy -trace print_requests.py //permissions/cluster-policies diff --git a/acceptance/bundle/resources/cluster_policies/permissions/basic/test.toml b/acceptance/bundle/resources/cluster_policies/permissions/basic/test.toml deleted file mode 100644 index c03a6d63274..00000000000 --- a/acceptance/bundle/resources/cluster_policies/permissions/basic/test.toml +++ /dev/null @@ -1,5 +0,0 @@ -# Local-only: this deterministic test uses a fixed policy name and does not mask -# the server id, so it is not safe for concurrent cloud runs. Cloud coverage of -# the permission behavior lives in the levels test. -Cloud = false -Ignore = [".databricks", "databricks.yml", "updated.yml"] diff --git a/acceptance/bundle/resources/cluster_policies/permissions/basic/databricks.yml b/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_deletion/databricks.yml similarity index 65% rename from acceptance/bundle/resources/cluster_policies/permissions/basic/databricks.yml rename to acceptance/bundle/resources/cluster_policies/permissions/out_of_band_deletion/databricks.yml index 7fd70916477..9615515a7c6 100644 --- a/acceptance/bundle/resources/cluster_policies/permissions/basic/databricks.yml +++ b/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_deletion/databricks.yml @@ -1,5 +1,5 @@ bundle: - name: test_cluster_policy_permissions + name: test_cluster_policy_permissions_recreate resources: cluster_policies: @@ -9,5 +9,3 @@ resources: permissions: - level: CAN_USE group_name: users - - level: CAN_USE # TO_REMOVE - user_name: viewer@example.com # TO_REMOVE diff --git a/acceptance/bundle/resources/cluster_policies/permissions/basic/out.test.toml b/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_deletion/out.test.toml similarity index 100% rename from acceptance/bundle/resources/cluster_policies/permissions/basic/out.test.toml rename to acceptance/bundle/resources/cluster_policies/permissions/out_of_band_deletion/out.test.toml diff --git a/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_deletion/output.txt b/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_deletion/output.txt new file mode 100644 index 00000000000..d1b2cc33b65 --- /dev/null +++ b/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_deletion/output.txt @@ -0,0 +1,58 @@ + +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test_cluster_policy_permissions_recreate/default/files... +Created cluster_policies.test_cluster_policy +Created cluster_policies.test_cluster_policy.permissions +Files: 5 uploaded, 0 deleted +Resources: 2 created, 0 changed, 0 deleted, 0 unchanged + +=== Permissions are set on the live policy +>>> [CLI] api get /api/2.0/permissions/cluster-policies/[TEST_CLUSTER_POLICY_ID] +{ + "object_type": "cluster-policy", + "access_control_list": [ + { + "all_permissions": [ + { + "inherited": false, + "permission_level": "CAN_USE" + } + ], + "group_name": "users" + } + ] +} + +=== Delete the policy out of band +>>> [CLI] cluster-policies delete [TEST_CLUSTER_POLICY_ID] + +=== Permissions API still returns the ACLs for the deleted policy (no 404) +>>> [CLI] api get /api/2.0/permissions/cluster-policies/[TEST_CLUSTER_POLICY_ID] +{ + "object_type": "cluster-policy", + "access_control_list": [ + { + "all_permissions": [ + { + "inherited": false, + "permission_level": "CAN_USE" + } + ], + "group_name": "users" + } + ] +} + +=== Plan recreates the gone policy +>>> [CLI] bundle plan +create cluster_policies.test_cluster_policy +update cluster_policies.test_cluster_policy.permissions + +Plan: 1 to add, 1 to change, 0 to delete, 0 unchanged + +>>> [CLI] bundle destroy --auto-approve +The following resources will be deleted: + +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test_cluster_policy_permissions_recreate/default + +Destroy: 1 deleted diff --git a/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_deletion/script b/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_deletion/script new file mode 100644 index 00000000000..d764e47ed7f --- /dev/null +++ b/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_deletion/script @@ -0,0 +1,22 @@ +cleanup() { + trace $CLI bundle destroy --auto-approve + rm -f out.requests.txt +} +trap cleanup EXIT + +trace $CLI bundle deploy +policy_id="$(read_id.py test_cluster_policy)" + +title "Permissions are set on the live policy" +trace $CLI api get /api/2.0/permissions/cluster-policies/$policy_id | jq '{object_type, access_control_list}' + +title "Delete the policy out of band" +trace $CLI cluster-policies delete "$policy_id" + +# v1 quirk: the permissions endpoint keeps returning the ACLs for a deleted +# cluster policy; it does not 404 the way a v2 cascade-deleted resource would. +title "Permissions API still returns the ACLs for the deleted policy (no 404)" +trace $CLI api get /api/2.0/permissions/cluster-policies/$policy_id | jq '{object_type, access_control_list}' + +title "Plan recreates the gone policy" +trace $CLI bundle plan diff --git a/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_deletion/test.toml b/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_deletion/test.toml new file mode 100644 index 00000000000..4a3471ddc43 --- /dev/null +++ b/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_deletion/test.toml @@ -0,0 +1,4 @@ +# Local-only: fixed policy name is not safe for concurrent cloud runs. Cloud +# coverage of the permission behavior lives in the levels test. +Cloud = false +Ignore = [".databricks", "databricks.yml"] From 5ee38969a341c44e8aa03b6b305baa2ac2176cee Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Mon, 24 Aug 2026 23:08:33 +0000 Subject: [PATCH 11/12] Fix Windows path conversion in out_of_band_deletion permission test The test passes a leading-slash path to `api get`; on Windows, Git Bash rewrites /api/... to a Windows path before the CLI sees it, so the request misses the testserver stub. Set Env.MSYS_NO_PATHCONV, matching the genie_spaces out_of_band_deletion precedent. Co-authored-by: Isaac --- .../permissions/out_of_band_deletion/test.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_deletion/test.toml b/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_deletion/test.toml index 4a3471ddc43..7bec79e9c63 100644 --- a/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_deletion/test.toml +++ b/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_deletion/test.toml @@ -1,4 +1,9 @@ # Local-only: fixed policy name is not safe for concurrent cloud runs. Cloud # coverage of the permission behavior lives in the levels test. Cloud = false + +# `api get` is passed a leading-slash path; without this, Git Bash on Windows +# rewrites /api/... to C:\Program Files\Git\api\... before the CLI sees it. +Env.MSYS_NO_PATHCONV = "1" + Ignore = [".databricks", "databricks.yml"] From 366aa22c662468d972370ca73b1a7b069f33e3ee Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Mon, 24 Aug 2026 23:30:12 +0000 Subject: [PATCH 12/12] Avoid read_id.py under MSYS_NO_PATHCONV in out_of_band_deletion With MSYS_NO_PATHCONV set (needed for the api get leading-slash path), Git Bash on Windows no longer converts the PATH-resolved location of the read_id.py helper, so python3 cannot open it. Fetch the policy id via bundle summary + add_repl (a shell function), matching the genie_spaces precedent. Golden output is unchanged. Co-authored-by: Isaac --- .../cluster_policies/permissions/out_of_band_deletion/script | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_deletion/script b/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_deletion/script index d764e47ed7f..fa6e94b8732 100644 --- a/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_deletion/script +++ b/acceptance/bundle/resources/cluster_policies/permissions/out_of_band_deletion/script @@ -5,7 +5,10 @@ cleanup() { trap cleanup EXIT trace $CLI bundle deploy -policy_id="$(read_id.py test_cluster_policy)" +# Fetch the id via summary + add_repl (a shell function) rather than read_id.py: +# MSYS_NO_PATHCONV mangles the PATH-resolved python script location on Windows. +policy_id=$($CLI bundle summary --output json | jq -r '.resources.cluster_policies.test_cluster_policy.id') +add_repl "$policy_id" TEST_CLUSTER_POLICY_ID title "Permissions are set on the live policy" trace $CLI api get /api/2.0/permissions/cluster-policies/$policy_id | jq '{object_type, access_control_list}'