From ee6b7a6e1239d6aea5352325d96da6d9b791ed41 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Fri, 20 Mar 2026 18:29:26 +0100 Subject: [PATCH 01/14] Add acceptance test reproducing duplicate grants error with ALL_PRIVILEGES The direct engine's applyGrants() puts ALL_PRIVILEGES in both Add and Remove lists, causing the backend to reject with "Duplicate privileges to add and delete for principal". Co-authored-by: Isaac --- .../schemas/all_privileges/databricks.yml.tmpl | 12 ++++++++++++ .../schemas/all_privileges/out.deploy.direct.txt | 10 ++++++++++ .../schemas/all_privileges/out.deploy.terraform.txt | 4 ++++ .../grants/schemas/all_privileges/out.test.toml | 6 ++++++ .../grants/schemas/all_privileges/output.txt | 12 ++++++++++++ .../resources/grants/schemas/all_privileges/script | 6 ++++++ .../grants/schemas/all_privileges/test.toml | 1 + 7 files changed, 51 insertions(+) create mode 100644 acceptance/bundle/resources/grants/schemas/all_privileges/databricks.yml.tmpl create mode 100644 acceptance/bundle/resources/grants/schemas/all_privileges/out.deploy.direct.txt create mode 100644 acceptance/bundle/resources/grants/schemas/all_privileges/out.deploy.terraform.txt create mode 100644 acceptance/bundle/resources/grants/schemas/all_privileges/out.test.toml create mode 100644 acceptance/bundle/resources/grants/schemas/all_privileges/output.txt create mode 100644 acceptance/bundle/resources/grants/schemas/all_privileges/script create mode 100644 acceptance/bundle/resources/grants/schemas/all_privileges/test.toml diff --git a/acceptance/bundle/resources/grants/schemas/all_privileges/databricks.yml.tmpl b/acceptance/bundle/resources/grants/schemas/all_privileges/databricks.yml.tmpl new file mode 100644 index 0000000000..851235b799 --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/all_privileges/databricks.yml.tmpl @@ -0,0 +1,12 @@ +bundle: + name: schema-dup-grants-$UNIQUE_NAME + +resources: + schemas: + apps_schema: + name: schema_dup_grants_$UNIQUE_NAME + catalog_name: main + grants: + - principal: deco-test-user@databricks.com + privileges: + - ALL_PRIVILEGES diff --git a/acceptance/bundle/resources/grants/schemas/all_privileges/out.deploy.direct.txt b/acceptance/bundle/resources/grants/schemas/all_privileges/out.deploy.direct.txt new file mode 100644 index 0000000000..6f11586663 --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/all_privileges/out.deploy.direct.txt @@ -0,0 +1,10 @@ +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/schema-dup-grants-[UNIQUE_NAME]/default/files... +Deploying resources... +Error: cannot create resources.schemas.apps_schema.grants: Duplicate privileges to add and delete for principal deco-test-user@databricks.com. (400 INVALID_PARAMETER_VALUE) + +Endpoint: ... +HTTP Status: 400 Bad Request +API error_code: INVALID_PARAMETER_VALUE +API message: Duplicate privileges to add and delete for principal deco-test-user@databricks.com. + +Updating deployment state... diff --git a/acceptance/bundle/resources/grants/schemas/all_privileges/out.deploy.terraform.txt b/acceptance/bundle/resources/grants/schemas/all_privileges/out.deploy.terraform.txt new file mode 100644 index 0000000000..3d92e04530 --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/all_privileges/out.deploy.terraform.txt @@ -0,0 +1,4 @@ +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/schema-dup-grants-[UNIQUE_NAME]/default/files... +Deploying resources... +Updating deployment state... +Deployment complete! diff --git a/acceptance/bundle/resources/grants/schemas/all_privileges/out.test.toml b/acceptance/bundle/resources/grants/schemas/all_privileges/out.test.toml new file mode 100644 index 0000000000..d61c11e25c --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/all_privileges/out.test.toml @@ -0,0 +1,6 @@ +Local = true +Cloud = true +RequiresUnityCatalog = true + +[EnvMatrix] + DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/bundle/resources/grants/schemas/all_privileges/output.txt b/acceptance/bundle/resources/grants/schemas/all_privileges/output.txt new file mode 100644 index 0000000000..dec0fb8f7f --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/all_privileges/output.txt @@ -0,0 +1,12 @@ + +>>> [CLI] bundle destroy --auto-approve +The following resources will be deleted: + delete resources.schemas.apps_schema + +This action will result in the deletion of the following UC schemas. Any underlying data may be lost: + delete resources.schemas.apps_schema + +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/schema-dup-grants-[UNIQUE_NAME]/default + +Deleting files... +Destroy complete! diff --git a/acceptance/bundle/resources/grants/schemas/all_privileges/script b/acceptance/bundle/resources/grants/schemas/all_privileges/script new file mode 100644 index 0000000000..1717512b55 --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/all_privileges/script @@ -0,0 +1,6 @@ +envsubst < databricks.yml.tmpl > databricks.yml + +# The direct engine puts ALL_PRIVILEGES in both the Add and Remove lists in the PATCH request, +# which the backend rejects with "Duplicate privileges to add and delete". +$CLI bundle deploy 2>&1 | sed 's/Endpoint: .*/Endpoint: .../' > out.deploy.$DATABRICKS_BUNDLE_ENGINE.txt || true +trace $CLI bundle destroy --auto-approve diff --git a/acceptance/bundle/resources/grants/schemas/all_privileges/test.toml b/acceptance/bundle/resources/grants/schemas/all_privileges/test.toml new file mode 100644 index 0000000000..a030353d57 --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/all_privileges/test.toml @@ -0,0 +1 @@ +RecordRequests = false From 8c8abd62b87bd7bb2a1f0a42befa77ab2f7d5673 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Fri, 20 Mar 2026 18:36:35 +0100 Subject: [PATCH 02/14] testserver: Reject duplicate privileges in Add and Remove for grants Match the real backend behavior that returns INVALID_PARAMETER_VALUE when the same privilege appears in both Add and Remove lists for a principal. Co-authored-by: Isaac --- libs/testserver/grants.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/libs/testserver/grants.go b/libs/testserver/grants.go index ad773ae8ea..2070a1b0e1 100644 --- a/libs/testserver/grants.go +++ b/libs/testserver/grants.go @@ -40,6 +40,25 @@ func (s *FakeWorkspace) GrantsUpdate(req Request, securableType, fullName string } } + // Validate: reject duplicate privileges in Add and Remove for the same principal + for _, change := range request.Changes { + addSet := make(map[catalog.Privilege]bool, len(change.Add)) + for _, p := range change.Add { + addSet[p] = true + } + for _, p := range change.Remove { + if addSet[p] { + return Response{ + StatusCode: http.StatusBadRequest, + Body: map[string]string{ + "error_code": "INVALID_PARAMETER_VALUE", + "message": fmt.Sprintf("Duplicate privileges to add and delete for principal %s.", change.Principal), + }, + } + } + } + } + // Apply changes for _, change := range request.Changes { if change.Principal == "" { From 85b4d50489d4e8a05e2fece879e7d53ab348b6cc Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Fri, 20 Mar 2026 18:45:19 +0100 Subject: [PATCH 03/14] Add duplicate_principals grants test; add Badness annotations - duplicate_principals: same principal listed twice with same privilege causes drift on direct engine after deploy - all_privileges: remove unnecessary sed, add Badness to both tests Co-authored-by: Isaac --- .../all_privileges/out.deploy.direct.txt | 2 +- .../grants/schemas/all_privileges/script | 2 +- .../grants/schemas/all_privileges/test.toml | 1 + .../duplicate_principals/databricks.yml.tmpl | 15 +++++++++++++++ .../duplicate_principals/out.plan.direct.txt | 5 +++++ .../out.plan.terraform.txt | 3 +++ .../schemas/duplicate_principals/out.test.toml | 6 ++++++ .../schemas/duplicate_principals/output.txt | 18 ++++++++++++++++++ .../grants/schemas/duplicate_principals/script | 6 ++++++ .../schemas/duplicate_principals/test.toml | 2 ++ 10 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 acceptance/bundle/resources/grants/schemas/duplicate_principals/databricks.yml.tmpl create mode 100644 acceptance/bundle/resources/grants/schemas/duplicate_principals/out.plan.direct.txt create mode 100644 acceptance/bundle/resources/grants/schemas/duplicate_principals/out.plan.terraform.txt create mode 100644 acceptance/bundle/resources/grants/schemas/duplicate_principals/out.test.toml create mode 100644 acceptance/bundle/resources/grants/schemas/duplicate_principals/output.txt create mode 100644 acceptance/bundle/resources/grants/schemas/duplicate_principals/script create mode 100644 acceptance/bundle/resources/grants/schemas/duplicate_principals/test.toml diff --git a/acceptance/bundle/resources/grants/schemas/all_privileges/out.deploy.direct.txt b/acceptance/bundle/resources/grants/schemas/all_privileges/out.deploy.direct.txt index 6f11586663..5d7cc26c63 100644 --- a/acceptance/bundle/resources/grants/schemas/all_privileges/out.deploy.direct.txt +++ b/acceptance/bundle/resources/grants/schemas/all_privileges/out.deploy.direct.txt @@ -2,7 +2,7 @@ Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/schema-dup-grants- Deploying resources... Error: cannot create resources.schemas.apps_schema.grants: Duplicate privileges to add and delete for principal deco-test-user@databricks.com. (400 INVALID_PARAMETER_VALUE) -Endpoint: ... +Endpoint: PATCH [DATABRICKS_URL]/api/2.1/unity-catalog/permissions/schema/main.schema_dup_grants_[UNIQUE_NAME] HTTP Status: 400 Bad Request API error_code: INVALID_PARAMETER_VALUE API message: Duplicate privileges to add and delete for principal deco-test-user@databricks.com. diff --git a/acceptance/bundle/resources/grants/schemas/all_privileges/script b/acceptance/bundle/resources/grants/schemas/all_privileges/script index 1717512b55..1aed4dd553 100644 --- a/acceptance/bundle/resources/grants/schemas/all_privileges/script +++ b/acceptance/bundle/resources/grants/schemas/all_privileges/script @@ -2,5 +2,5 @@ envsubst < databricks.yml.tmpl > databricks.yml # The direct engine puts ALL_PRIVILEGES in both the Add and Remove lists in the PATCH request, # which the backend rejects with "Duplicate privileges to add and delete". -$CLI bundle deploy 2>&1 | sed 's/Endpoint: .*/Endpoint: .../' > out.deploy.$DATABRICKS_BUNDLE_ENGINE.txt || true +$CLI bundle deploy > out.deploy.$DATABRICKS_BUNDLE_ENGINE.txt 2>&1 || true trace $CLI bundle destroy --auto-approve diff --git a/acceptance/bundle/resources/grants/schemas/all_privileges/test.toml b/acceptance/bundle/resources/grants/schemas/all_privileges/test.toml index a030353d57..6a926a4f97 100644 --- a/acceptance/bundle/resources/grants/schemas/all_privileges/test.toml +++ b/acceptance/bundle/resources/grants/schemas/all_privileges/test.toml @@ -1 +1,2 @@ +Badness = "direct engine fails: applyGrants() puts ALL_PRIVILEGES in both Add and Remove lists" RecordRequests = false diff --git a/acceptance/bundle/resources/grants/schemas/duplicate_principals/databricks.yml.tmpl b/acceptance/bundle/resources/grants/schemas/duplicate_principals/databricks.yml.tmpl new file mode 100644 index 0000000000..a95dcaab78 --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/duplicate_principals/databricks.yml.tmpl @@ -0,0 +1,15 @@ +bundle: + name: schema-dup-grants-$UNIQUE_NAME + +resources: + schemas: + apps_schema: + name: schema_dup_grants_$UNIQUE_NAME + catalog_name: main + grants: + - principal: deco-test-user@databricks.com + privileges: + - CREATE_TABLE + - principal: deco-test-user@databricks.com + privileges: + - CREATE_TABLE diff --git a/acceptance/bundle/resources/grants/schemas/duplicate_principals/out.plan.direct.txt b/acceptance/bundle/resources/grants/schemas/duplicate_principals/out.plan.direct.txt new file mode 100644 index 0000000000..746092b5c2 --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/duplicate_principals/out.plan.direct.txt @@ -0,0 +1,5 @@ + +>>> [CLI] bundle plan +update schemas.apps_schema.grants + +Plan: 0 to add, 1 to change, 0 to delete, 1 unchanged diff --git a/acceptance/bundle/resources/grants/schemas/duplicate_principals/out.plan.terraform.txt b/acceptance/bundle/resources/grants/schemas/duplicate_principals/out.plan.terraform.txt new file mode 100644 index 0000000000..068a177d51 --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/duplicate_principals/out.plan.terraform.txt @@ -0,0 +1,3 @@ + +>>> [CLI] bundle plan +Plan: 0 to add, 0 to change, 0 to delete, 2 unchanged diff --git a/acceptance/bundle/resources/grants/schemas/duplicate_principals/out.test.toml b/acceptance/bundle/resources/grants/schemas/duplicate_principals/out.test.toml new file mode 100644 index 0000000000..d61c11e25c --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/duplicate_principals/out.test.toml @@ -0,0 +1,6 @@ +Local = true +Cloud = true +RequiresUnityCatalog = true + +[EnvMatrix] + DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/bundle/resources/grants/schemas/duplicate_principals/output.txt b/acceptance/bundle/resources/grants/schemas/duplicate_principals/output.txt new file mode 100644 index 0000000000..a537487dbd --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/duplicate_principals/output.txt @@ -0,0 +1,18 @@ + +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/schema-dup-grants-[UNIQUE_NAME]/default/files... +Deploying resources... +Updating deployment state... +Deployment complete! + +>>> [CLI] bundle destroy --auto-approve +The following resources will be deleted: + delete resources.schemas.apps_schema + +This action will result in the deletion of the following UC schemas. Any underlying data may be lost: + delete resources.schemas.apps_schema + +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/schema-dup-grants-[UNIQUE_NAME]/default + +Deleting files... +Destroy complete! diff --git a/acceptance/bundle/resources/grants/schemas/duplicate_principals/script b/acceptance/bundle/resources/grants/schemas/duplicate_principals/script new file mode 100644 index 0000000000..1a72d57534 --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/duplicate_principals/script @@ -0,0 +1,6 @@ +envsubst < databricks.yml.tmpl > databricks.yml + +# Same principal listed twice with the same privilege. +trace $CLI bundle deploy +trace $CLI bundle plan > out.plan.$DATABRICKS_BUNDLE_ENGINE.txt 2>&1 +trace $CLI bundle destroy --auto-approve diff --git a/acceptance/bundle/resources/grants/schemas/duplicate_principals/test.toml b/acceptance/bundle/resources/grants/schemas/duplicate_principals/test.toml new file mode 100644 index 0000000000..03c367be56 --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/duplicate_principals/test.toml @@ -0,0 +1,2 @@ +Badness = "direct engine shows drift after deploy when same principal is listed twice with same privilege" +RecordRequests = false From 8660651f716fb63efee569f5682b98529de36914 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Fri, 20 Mar 2026 18:49:35 +0100 Subject: [PATCH 04/14] Add duplicate_privileges grants test; use cleanup() trap in all grant tests - duplicate_privileges: same privilege listed twice for same principal causes drift on direct engine - Use cleanup() function with trap EXIT for destroy in all three tests Co-authored-by: Isaac --- .../grants/schemas/all_privileges/script | 6 +++++- .../grants/schemas/duplicate_principals/script | 6 +++++- .../duplicate_privileges/databricks.yml.tmpl | 13 +++++++++++++ .../duplicate_privileges/out.plan.direct.txt | 5 +++++ .../out.plan.terraform.txt | 3 +++ .../schemas/duplicate_privileges/out.test.toml | 6 ++++++ .../schemas/duplicate_privileges/output.txt | 18 ++++++++++++++++++ .../grants/schemas/duplicate_privileges/script | 10 ++++++++++ .../schemas/duplicate_privileges/test.toml | 2 ++ 9 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 acceptance/bundle/resources/grants/schemas/duplicate_privileges/databricks.yml.tmpl create mode 100644 acceptance/bundle/resources/grants/schemas/duplicate_privileges/out.plan.direct.txt create mode 100644 acceptance/bundle/resources/grants/schemas/duplicate_privileges/out.plan.terraform.txt create mode 100644 acceptance/bundle/resources/grants/schemas/duplicate_privileges/out.test.toml create mode 100644 acceptance/bundle/resources/grants/schemas/duplicate_privileges/output.txt create mode 100644 acceptance/bundle/resources/grants/schemas/duplicate_privileges/script create mode 100644 acceptance/bundle/resources/grants/schemas/duplicate_privileges/test.toml diff --git a/acceptance/bundle/resources/grants/schemas/all_privileges/script b/acceptance/bundle/resources/grants/schemas/all_privileges/script index 1aed4dd553..3f217e6a40 100644 --- a/acceptance/bundle/resources/grants/schemas/all_privileges/script +++ b/acceptance/bundle/resources/grants/schemas/all_privileges/script @@ -1,6 +1,10 @@ envsubst < databricks.yml.tmpl > databricks.yml +cleanup() { + trace $CLI bundle destroy --auto-approve +} +trap cleanup EXIT + # The direct engine puts ALL_PRIVILEGES in both the Add and Remove lists in the PATCH request, # which the backend rejects with "Duplicate privileges to add and delete". $CLI bundle deploy > out.deploy.$DATABRICKS_BUNDLE_ENGINE.txt 2>&1 || true -trace $CLI bundle destroy --auto-approve diff --git a/acceptance/bundle/resources/grants/schemas/duplicate_principals/script b/acceptance/bundle/resources/grants/schemas/duplicate_principals/script index 1a72d57534..7610799f43 100644 --- a/acceptance/bundle/resources/grants/schemas/duplicate_principals/script +++ b/acceptance/bundle/resources/grants/schemas/duplicate_principals/script @@ -1,6 +1,10 @@ envsubst < databricks.yml.tmpl > databricks.yml +cleanup() { + trace $CLI bundle destroy --auto-approve +} +trap cleanup EXIT + # Same principal listed twice with the same privilege. trace $CLI bundle deploy trace $CLI bundle plan > out.plan.$DATABRICKS_BUNDLE_ENGINE.txt 2>&1 -trace $CLI bundle destroy --auto-approve diff --git a/acceptance/bundle/resources/grants/schemas/duplicate_privileges/databricks.yml.tmpl b/acceptance/bundle/resources/grants/schemas/duplicate_privileges/databricks.yml.tmpl new file mode 100644 index 0000000000..aa04d5befb --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/duplicate_privileges/databricks.yml.tmpl @@ -0,0 +1,13 @@ +bundle: + name: schema-dup-grants-$UNIQUE_NAME + +resources: + schemas: + apps_schema: + name: schema_dup_grants_$UNIQUE_NAME + catalog_name: main + grants: + - principal: deco-test-user@databricks.com + privileges: + - CREATE_TABLE + - CREATE_TABLE diff --git a/acceptance/bundle/resources/grants/schemas/duplicate_privileges/out.plan.direct.txt b/acceptance/bundle/resources/grants/schemas/duplicate_privileges/out.plan.direct.txt new file mode 100644 index 0000000000..746092b5c2 --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/duplicate_privileges/out.plan.direct.txt @@ -0,0 +1,5 @@ + +>>> [CLI] bundle plan +update schemas.apps_schema.grants + +Plan: 0 to add, 1 to change, 0 to delete, 1 unchanged diff --git a/acceptance/bundle/resources/grants/schemas/duplicate_privileges/out.plan.terraform.txt b/acceptance/bundle/resources/grants/schemas/duplicate_privileges/out.plan.terraform.txt new file mode 100644 index 0000000000..068a177d51 --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/duplicate_privileges/out.plan.terraform.txt @@ -0,0 +1,3 @@ + +>>> [CLI] bundle plan +Plan: 0 to add, 0 to change, 0 to delete, 2 unchanged diff --git a/acceptance/bundle/resources/grants/schemas/duplicate_privileges/out.test.toml b/acceptance/bundle/resources/grants/schemas/duplicate_privileges/out.test.toml new file mode 100644 index 0000000000..d61c11e25c --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/duplicate_privileges/out.test.toml @@ -0,0 +1,6 @@ +Local = true +Cloud = true +RequiresUnityCatalog = true + +[EnvMatrix] + DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/bundle/resources/grants/schemas/duplicate_privileges/output.txt b/acceptance/bundle/resources/grants/schemas/duplicate_privileges/output.txt new file mode 100644 index 0000000000..a537487dbd --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/duplicate_privileges/output.txt @@ -0,0 +1,18 @@ + +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/schema-dup-grants-[UNIQUE_NAME]/default/files... +Deploying resources... +Updating deployment state... +Deployment complete! + +>>> [CLI] bundle destroy --auto-approve +The following resources will be deleted: + delete resources.schemas.apps_schema + +This action will result in the deletion of the following UC schemas. Any underlying data may be lost: + delete resources.schemas.apps_schema + +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/schema-dup-grants-[UNIQUE_NAME]/default + +Deleting files... +Destroy complete! diff --git a/acceptance/bundle/resources/grants/schemas/duplicate_privileges/script b/acceptance/bundle/resources/grants/schemas/duplicate_privileges/script new file mode 100644 index 0000000000..048c525dec --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/duplicate_privileges/script @@ -0,0 +1,10 @@ +envsubst < databricks.yml.tmpl > databricks.yml + +cleanup() { + trace $CLI bundle destroy --auto-approve +} +trap cleanup EXIT + +# Same privilege listed twice for the same principal in a single entry. +trace $CLI bundle deploy +trace $CLI bundle plan > out.plan.$DATABRICKS_BUNDLE_ENGINE.txt 2>&1 diff --git a/acceptance/bundle/resources/grants/schemas/duplicate_privileges/test.toml b/acceptance/bundle/resources/grants/schemas/duplicate_privileges/test.toml new file mode 100644 index 0000000000..ea40188536 --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/duplicate_privileges/test.toml @@ -0,0 +1,2 @@ +Badness = "direct engine shows drift after deploy when same privilege is listed twice for same principal" +RecordRequests = false From cbfc4d38409d853eaec86b87cc4104d7e7d94fbe Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Fri, 20 Mar 2026 18:54:13 +0100 Subject: [PATCH 05/14] Add per-engine grants request logging to duplicate grant tests Show the actual PATCH requests to the permissions endpoint for each engine, making the differences visible (e.g. direct sends ALL_PRIVILEGES in both Add and Remove, terraform sends clean requests). Co-authored-by: Isaac --- .../all_privileges/out.requests.direct.txt | 17 ++++++++++++ .../all_privileges/out.requests.terraform.txt | 26 +++++++++++++++++++ .../grants/schemas/all_privileges/script | 2 ++ .../grants/schemas/all_privileges/test.toml | 2 +- .../out.requests.direct.txt | 26 +++++++++++++++++++ .../out.requests.terraform.txt | 26 +++++++++++++++++++ .../schemas/duplicate_principals/script | 2 ++ .../schemas/duplicate_principals/test.toml | 2 +- .../out.requests.direct.txt | 18 +++++++++++++ .../out.requests.terraform.txt | 26 +++++++++++++++++++ .../schemas/duplicate_privileges/script | 2 ++ .../schemas/duplicate_privileges/test.toml | 2 +- 12 files changed, 148 insertions(+), 3 deletions(-) create mode 100644 acceptance/bundle/resources/grants/schemas/all_privileges/out.requests.direct.txt create mode 100644 acceptance/bundle/resources/grants/schemas/all_privileges/out.requests.terraform.txt create mode 100644 acceptance/bundle/resources/grants/schemas/duplicate_principals/out.requests.direct.txt create mode 100644 acceptance/bundle/resources/grants/schemas/duplicate_principals/out.requests.terraform.txt create mode 100644 acceptance/bundle/resources/grants/schemas/duplicate_privileges/out.requests.direct.txt create mode 100644 acceptance/bundle/resources/grants/schemas/duplicate_privileges/out.requests.terraform.txt diff --git a/acceptance/bundle/resources/grants/schemas/all_privileges/out.requests.direct.txt b/acceptance/bundle/resources/grants/schemas/all_privileges/out.requests.direct.txt new file mode 100644 index 0000000000..005006f47e --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/all_privileges/out.requests.direct.txt @@ -0,0 +1,17 @@ +{ + "method": "PATCH", + "path": "/api/2.1/unity-catalog/permissions/schema/main.schema_dup_grants_[UNIQUE_NAME]", + "body": { + "changes": [ + { + "add": [ + "ALL_PRIVILEGES" + ], + "principal": "deco-test-user@databricks.com", + "remove": [ + "ALL_PRIVILEGES" + ] + } + ] + } +} diff --git a/acceptance/bundle/resources/grants/schemas/all_privileges/out.requests.terraform.txt b/acceptance/bundle/resources/grants/schemas/all_privileges/out.requests.terraform.txt new file mode 100644 index 0000000000..5fc2fe369b --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/all_privileges/out.requests.terraform.txt @@ -0,0 +1,26 @@ +{ + "method": "GET", + "path": "/api/2.1/unity-catalog/permissions/schema/main.schema_dup_grants_[UNIQUE_NAME]" +} +{ + "method": "PATCH", + "path": "/api/2.1/unity-catalog/permissions/schema/main.schema_dup_grants_[UNIQUE_NAME]", + "body": { + "changes": [ + { + "add": [ + "ALL_PRIVILEGES" + ], + "principal": "deco-test-user@databricks.com" + } + ] + } +} +{ + "method": "GET", + "path": "/api/2.1/unity-catalog/permissions/schema/main.schema_dup_grants_[UNIQUE_NAME]" +} +{ + "method": "GET", + "path": "/api/2.1/unity-catalog/permissions/schema/main.schema_dup_grants_[UNIQUE_NAME]" +} diff --git a/acceptance/bundle/resources/grants/schemas/all_privileges/script b/acceptance/bundle/resources/grants/schemas/all_privileges/script index 3f217e6a40..b379108d29 100644 --- a/acceptance/bundle/resources/grants/schemas/all_privileges/script +++ b/acceptance/bundle/resources/grants/schemas/all_privileges/script @@ -2,9 +2,11 @@ envsubst < databricks.yml.tmpl > databricks.yml cleanup() { trace $CLI bundle destroy --auto-approve + rm -f out.requests.txt } trap cleanup EXIT # The direct engine puts ALL_PRIVILEGES in both the Add and Remove lists in the PATCH request, # which the backend rejects with "Duplicate privileges to add and delete". $CLI bundle deploy > out.deploy.$DATABRICKS_BUNDLE_ENGINE.txt 2>&1 || true +print_requests.py --get //permissions --keep > out.requests.$DATABRICKS_BUNDLE_ENGINE.txt diff --git a/acceptance/bundle/resources/grants/schemas/all_privileges/test.toml b/acceptance/bundle/resources/grants/schemas/all_privileges/test.toml index 6a926a4f97..901915dc62 100644 --- a/acceptance/bundle/resources/grants/schemas/all_privileges/test.toml +++ b/acceptance/bundle/resources/grants/schemas/all_privileges/test.toml @@ -1,2 +1,2 @@ Badness = "direct engine fails: applyGrants() puts ALL_PRIVILEGES in both Add and Remove lists" -RecordRequests = false +RecordRequests = true diff --git a/acceptance/bundle/resources/grants/schemas/duplicate_principals/out.requests.direct.txt b/acceptance/bundle/resources/grants/schemas/duplicate_principals/out.requests.direct.txt new file mode 100644 index 0000000000..2939c8fa30 --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/duplicate_principals/out.requests.direct.txt @@ -0,0 +1,26 @@ +{ + "method": "PATCH", + "path": "/api/2.1/unity-catalog/permissions/schema/main.schema_dup_grants_[UNIQUE_NAME]", + "body": { + "changes": [ + { + "add": [ + "CREATE_TABLE" + ], + "principal": "deco-test-user@databricks.com", + "remove": [ + "ALL_PRIVILEGES" + ] + }, + { + "add": [ + "CREATE_TABLE" + ], + "principal": "deco-test-user@databricks.com", + "remove": [ + "ALL_PRIVILEGES" + ] + } + ] + } +} diff --git a/acceptance/bundle/resources/grants/schemas/duplicate_principals/out.requests.terraform.txt b/acceptance/bundle/resources/grants/schemas/duplicate_principals/out.requests.terraform.txt new file mode 100644 index 0000000000..fdb92f6f3d --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/duplicate_principals/out.requests.terraform.txt @@ -0,0 +1,26 @@ +{ + "method": "GET", + "path": "/api/2.1/unity-catalog/permissions/schema/main.schema_dup_grants_[UNIQUE_NAME]" +} +{ + "method": "PATCH", + "path": "/api/2.1/unity-catalog/permissions/schema/main.schema_dup_grants_[UNIQUE_NAME]", + "body": { + "changes": [ + { + "add": [ + "CREATE_TABLE" + ], + "principal": "deco-test-user@databricks.com" + } + ] + } +} +{ + "method": "GET", + "path": "/api/2.1/unity-catalog/permissions/schema/main.schema_dup_grants_[UNIQUE_NAME]" +} +{ + "method": "GET", + "path": "/api/2.1/unity-catalog/permissions/schema/main.schema_dup_grants_[UNIQUE_NAME]" +} diff --git a/acceptance/bundle/resources/grants/schemas/duplicate_principals/script b/acceptance/bundle/resources/grants/schemas/duplicate_principals/script index 7610799f43..2929bacd17 100644 --- a/acceptance/bundle/resources/grants/schemas/duplicate_principals/script +++ b/acceptance/bundle/resources/grants/schemas/duplicate_principals/script @@ -2,9 +2,11 @@ envsubst < databricks.yml.tmpl > databricks.yml cleanup() { trace $CLI bundle destroy --auto-approve + rm -f out.requests.txt } trap cleanup EXIT # Same principal listed twice with the same privilege. trace $CLI bundle deploy +print_requests.py --get //permissions --keep > out.requests.$DATABRICKS_BUNDLE_ENGINE.txt trace $CLI bundle plan > out.plan.$DATABRICKS_BUNDLE_ENGINE.txt 2>&1 diff --git a/acceptance/bundle/resources/grants/schemas/duplicate_principals/test.toml b/acceptance/bundle/resources/grants/schemas/duplicate_principals/test.toml index 03c367be56..9acdd5d2b4 100644 --- a/acceptance/bundle/resources/grants/schemas/duplicate_principals/test.toml +++ b/acceptance/bundle/resources/grants/schemas/duplicate_principals/test.toml @@ -1,2 +1,2 @@ Badness = "direct engine shows drift after deploy when same principal is listed twice with same privilege" -RecordRequests = false +RecordRequests = true diff --git a/acceptance/bundle/resources/grants/schemas/duplicate_privileges/out.requests.direct.txt b/acceptance/bundle/resources/grants/schemas/duplicate_privileges/out.requests.direct.txt new file mode 100644 index 0000000000..f6597b1fa7 --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/duplicate_privileges/out.requests.direct.txt @@ -0,0 +1,18 @@ +{ + "method": "PATCH", + "path": "/api/2.1/unity-catalog/permissions/schema/main.schema_dup_grants_[UNIQUE_NAME]", + "body": { + "changes": [ + { + "add": [ + "CREATE_TABLE", + "CREATE_TABLE" + ], + "principal": "deco-test-user@databricks.com", + "remove": [ + "ALL_PRIVILEGES" + ] + } + ] + } +} diff --git a/acceptance/bundle/resources/grants/schemas/duplicate_privileges/out.requests.terraform.txt b/acceptance/bundle/resources/grants/schemas/duplicate_privileges/out.requests.terraform.txt new file mode 100644 index 0000000000..fdb92f6f3d --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/duplicate_privileges/out.requests.terraform.txt @@ -0,0 +1,26 @@ +{ + "method": "GET", + "path": "/api/2.1/unity-catalog/permissions/schema/main.schema_dup_grants_[UNIQUE_NAME]" +} +{ + "method": "PATCH", + "path": "/api/2.1/unity-catalog/permissions/schema/main.schema_dup_grants_[UNIQUE_NAME]", + "body": { + "changes": [ + { + "add": [ + "CREATE_TABLE" + ], + "principal": "deco-test-user@databricks.com" + } + ] + } +} +{ + "method": "GET", + "path": "/api/2.1/unity-catalog/permissions/schema/main.schema_dup_grants_[UNIQUE_NAME]" +} +{ + "method": "GET", + "path": "/api/2.1/unity-catalog/permissions/schema/main.schema_dup_grants_[UNIQUE_NAME]" +} diff --git a/acceptance/bundle/resources/grants/schemas/duplicate_privileges/script b/acceptance/bundle/resources/grants/schemas/duplicate_privileges/script index 048c525dec..6118156ba5 100644 --- a/acceptance/bundle/resources/grants/schemas/duplicate_privileges/script +++ b/acceptance/bundle/resources/grants/schemas/duplicate_privileges/script @@ -2,9 +2,11 @@ envsubst < databricks.yml.tmpl > databricks.yml cleanup() { trace $CLI bundle destroy --auto-approve + rm -f out.requests.txt } trap cleanup EXIT # Same privilege listed twice for the same principal in a single entry. trace $CLI bundle deploy +print_requests.py --get //permissions --keep > out.requests.$DATABRICKS_BUNDLE_ENGINE.txt trace $CLI bundle plan > out.plan.$DATABRICKS_BUNDLE_ENGINE.txt 2>&1 diff --git a/acceptance/bundle/resources/grants/schemas/duplicate_privileges/test.toml b/acceptance/bundle/resources/grants/schemas/duplicate_privileges/test.toml index ea40188536..097367fa01 100644 --- a/acceptance/bundle/resources/grants/schemas/duplicate_privileges/test.toml +++ b/acceptance/bundle/resources/grants/schemas/duplicate_privileges/test.toml @@ -1,2 +1,2 @@ Badness = "direct engine shows drift after deploy when same privilege is listed twice for same principal" -RecordRequests = false +RecordRequests = true From fb384b4f86f62b9645fcd64b4f4a7b1b1e209b16 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Fri, 20 Mar 2026 19:55:38 +0100 Subject: [PATCH 06/14] Fix duplicate grants issues in direct engine's applyGrants() Merge privileges by principal and deduplicate before sending to the API. Skip adding ALL_PRIVILEGES to the Remove list when it is being granted, preventing the "Duplicate privileges to add and delete" backend error. Co-authored-by: Isaac --- .../all_privileges/out.deploy.direct.txt | 8 +-- .../all_privileges/out.requests.direct.txt | 5 +- .../grants/schemas/all_privileges/test.toml | 1 - .../out.requests.direct.txt | 9 ---- .../schemas/duplicate_principals/test.toml | 1 - .../out.requests.direct.txt | 1 - .../schemas/duplicate_privileges/test.toml | 1 - bundle/direct/dresources/grants.go | 54 +++++++++++++++---- 8 files changed, 47 insertions(+), 33 deletions(-) diff --git a/acceptance/bundle/resources/grants/schemas/all_privileges/out.deploy.direct.txt b/acceptance/bundle/resources/grants/schemas/all_privileges/out.deploy.direct.txt index 5d7cc26c63..3d92e04530 100644 --- a/acceptance/bundle/resources/grants/schemas/all_privileges/out.deploy.direct.txt +++ b/acceptance/bundle/resources/grants/schemas/all_privileges/out.deploy.direct.txt @@ -1,10 +1,4 @@ Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/schema-dup-grants-[UNIQUE_NAME]/default/files... Deploying resources... -Error: cannot create resources.schemas.apps_schema.grants: Duplicate privileges to add and delete for principal deco-test-user@databricks.com. (400 INVALID_PARAMETER_VALUE) - -Endpoint: PATCH [DATABRICKS_URL]/api/2.1/unity-catalog/permissions/schema/main.schema_dup_grants_[UNIQUE_NAME] -HTTP Status: 400 Bad Request -API error_code: INVALID_PARAMETER_VALUE -API message: Duplicate privileges to add and delete for principal deco-test-user@databricks.com. - Updating deployment state... +Deployment complete! diff --git a/acceptance/bundle/resources/grants/schemas/all_privileges/out.requests.direct.txt b/acceptance/bundle/resources/grants/schemas/all_privileges/out.requests.direct.txt index 005006f47e..d7cef386ad 100644 --- a/acceptance/bundle/resources/grants/schemas/all_privileges/out.requests.direct.txt +++ b/acceptance/bundle/resources/grants/schemas/all_privileges/out.requests.direct.txt @@ -7,10 +7,7 @@ "add": [ "ALL_PRIVILEGES" ], - "principal": "deco-test-user@databricks.com", - "remove": [ - "ALL_PRIVILEGES" - ] + "principal": "deco-test-user@databricks.com" } ] } diff --git a/acceptance/bundle/resources/grants/schemas/all_privileges/test.toml b/acceptance/bundle/resources/grants/schemas/all_privileges/test.toml index 901915dc62..159efe0269 100644 --- a/acceptance/bundle/resources/grants/schemas/all_privileges/test.toml +++ b/acceptance/bundle/resources/grants/schemas/all_privileges/test.toml @@ -1,2 +1 @@ -Badness = "direct engine fails: applyGrants() puts ALL_PRIVILEGES in both Add and Remove lists" RecordRequests = true diff --git a/acceptance/bundle/resources/grants/schemas/duplicate_principals/out.requests.direct.txt b/acceptance/bundle/resources/grants/schemas/duplicate_principals/out.requests.direct.txt index 2939c8fa30..a0d3144160 100644 --- a/acceptance/bundle/resources/grants/schemas/duplicate_principals/out.requests.direct.txt +++ b/acceptance/bundle/resources/grants/schemas/duplicate_principals/out.requests.direct.txt @@ -3,15 +3,6 @@ "path": "/api/2.1/unity-catalog/permissions/schema/main.schema_dup_grants_[UNIQUE_NAME]", "body": { "changes": [ - { - "add": [ - "CREATE_TABLE" - ], - "principal": "deco-test-user@databricks.com", - "remove": [ - "ALL_PRIVILEGES" - ] - }, { "add": [ "CREATE_TABLE" diff --git a/acceptance/bundle/resources/grants/schemas/duplicate_principals/test.toml b/acceptance/bundle/resources/grants/schemas/duplicate_principals/test.toml index 9acdd5d2b4..159efe0269 100644 --- a/acceptance/bundle/resources/grants/schemas/duplicate_principals/test.toml +++ b/acceptance/bundle/resources/grants/schemas/duplicate_principals/test.toml @@ -1,2 +1 @@ -Badness = "direct engine shows drift after deploy when same principal is listed twice with same privilege" RecordRequests = true diff --git a/acceptance/bundle/resources/grants/schemas/duplicate_privileges/out.requests.direct.txt b/acceptance/bundle/resources/grants/schemas/duplicate_privileges/out.requests.direct.txt index f6597b1fa7..a0d3144160 100644 --- a/acceptance/bundle/resources/grants/schemas/duplicate_privileges/out.requests.direct.txt +++ b/acceptance/bundle/resources/grants/schemas/duplicate_privileges/out.requests.direct.txt @@ -5,7 +5,6 @@ "changes": [ { "add": [ - "CREATE_TABLE", "CREATE_TABLE" ], "principal": "deco-test-user@databricks.com", diff --git a/acceptance/bundle/resources/grants/schemas/duplicate_privileges/test.toml b/acceptance/bundle/resources/grants/schemas/duplicate_privileges/test.toml index 097367fa01..159efe0269 100644 --- a/acceptance/bundle/resources/grants/schemas/duplicate_privileges/test.toml +++ b/acceptance/bundle/resources/grants/schemas/duplicate_privileges/test.toml @@ -1,2 +1 @@ -Badness = "direct engine shows drift after deploy when same privilege is listed twice for same principal" RecordRequests = true diff --git a/bundle/direct/dresources/grants.go b/bundle/direct/dresources/grants.go index 00c8281aa3..b6ae1e5cc1 100644 --- a/bundle/direct/dresources/grants.go +++ b/bundle/direct/dresources/grants.go @@ -130,16 +130,21 @@ func (r *ResourceGrants) applyGrants(ctx context.Context, state *GrantsState) er return errors.New("internal error: grants full_name must be resolved before deployment") } - var changes []catalog.PermissionsChange + // Merge privileges by principal and deduplicate. + merged := mergeGrantAssignments(state.EmbeddedSlice) - // For each principal in the config, add their grants and remove everything else - for _, grantAssignment := range state.EmbeddedSlice { - changes = append(changes, catalog.PermissionsChange{ - Principal: grantAssignment.Principal, - Add: grantAssignment.Privileges, - Remove: []catalog.Privilege{catalog.PrivilegeAllPrivileges}, - ForceSendFields: nil, - }) + var changes []catalog.PermissionsChange + for _, ga := range merged { + change := catalog.PermissionsChange{ + Principal: ga.Principal, + Add: ga.Privileges, + } + // Remove all other privileges unless ALL_PRIVILEGES is being granted + // (it would conflict with appearing in both Add and Remove). + if !slices.Contains(ga.Privileges, catalog.PrivilegeAllPrivileges) { + change.Remove = []catalog.Privilege{catalog.PrivilegeAllPrivileges} + } + changes = append(changes, change) } _, err := r.client.Grants.Update(ctx, catalog.UpdatePermissions{ @@ -150,6 +155,37 @@ func (r *ResourceGrants) applyGrants(ctx context.Context, state *GrantsState) er return err } +// mergeGrantAssignments consolidates multiple entries for the same principal +// into a single entry with deduplicated, sorted privileges. +func mergeGrantAssignments(assignments []catalog.PrivilegeAssignment) []catalog.PrivilegeAssignment { + seen := map[string]map[catalog.Privilege]bool{} + var order []string + + for _, a := range assignments { + if seen[a.Principal] == nil { + seen[a.Principal] = map[catalog.Privilege]bool{} + order = append(order, a.Principal) + } + for _, p := range a.Privileges { + seen[a.Principal][p] = true + } + } + + result := make([]catalog.PrivilegeAssignment, 0, len(order)) + for _, principal := range order { + privs := make([]catalog.Privilege, 0, len(seen[principal])) + for p := range seen[principal] { + privs = append(privs, p) + } + slices.Sort(privs) + result = append(result, catalog.PrivilegeAssignment{ + Principal: principal, + Privileges: privs, + }) + } + return result +} + func (r *ResourceGrants) listGrants(ctx context.Context, securableType, fullName string) ([]catalog.PrivilegeAssignment, error) { var assignments []catalog.PrivilegeAssignment pageToken := "" From 5b64180717561927b7b3be02ab862c00da7fd151 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Mon, 23 Mar 2026 09:52:14 +0100 Subject: [PATCH 07/14] clean up test --- .../grants/schemas/all_privileges/out.deploy.direct.txt | 4 ---- .../grants/schemas/all_privileges/out.deploy.terraform.txt | 4 ---- .../bundle/resources/grants/schemas/all_privileges/output.txt | 4 ++++ .../bundle/resources/grants/schemas/all_privileges/script | 2 +- 4 files changed, 5 insertions(+), 9 deletions(-) delete mode 100644 acceptance/bundle/resources/grants/schemas/all_privileges/out.deploy.direct.txt delete mode 100644 acceptance/bundle/resources/grants/schemas/all_privileges/out.deploy.terraform.txt diff --git a/acceptance/bundle/resources/grants/schemas/all_privileges/out.deploy.direct.txt b/acceptance/bundle/resources/grants/schemas/all_privileges/out.deploy.direct.txt deleted file mode 100644 index 3d92e04530..0000000000 --- a/acceptance/bundle/resources/grants/schemas/all_privileges/out.deploy.direct.txt +++ /dev/null @@ -1,4 +0,0 @@ -Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/schema-dup-grants-[UNIQUE_NAME]/default/files... -Deploying resources... -Updating deployment state... -Deployment complete! diff --git a/acceptance/bundle/resources/grants/schemas/all_privileges/out.deploy.terraform.txt b/acceptance/bundle/resources/grants/schemas/all_privileges/out.deploy.terraform.txt deleted file mode 100644 index 3d92e04530..0000000000 --- a/acceptance/bundle/resources/grants/schemas/all_privileges/out.deploy.terraform.txt +++ /dev/null @@ -1,4 +0,0 @@ -Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/schema-dup-grants-[UNIQUE_NAME]/default/files... -Deploying resources... -Updating deployment state... -Deployment complete! diff --git a/acceptance/bundle/resources/grants/schemas/all_privileges/output.txt b/acceptance/bundle/resources/grants/schemas/all_privileges/output.txt index dec0fb8f7f..e88c30715f 100644 --- a/acceptance/bundle/resources/grants/schemas/all_privileges/output.txt +++ b/acceptance/bundle/resources/grants/schemas/all_privileges/output.txt @@ -1,3 +1,7 @@ +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/schema-dup-grants-[UNIQUE_NAME]/default/files... +Deploying resources... +Updating deployment state... +Deployment complete! >>> [CLI] bundle destroy --auto-approve The following resources will be deleted: diff --git a/acceptance/bundle/resources/grants/schemas/all_privileges/script b/acceptance/bundle/resources/grants/schemas/all_privileges/script index b379108d29..e8f6e243dd 100644 --- a/acceptance/bundle/resources/grants/schemas/all_privileges/script +++ b/acceptance/bundle/resources/grants/schemas/all_privileges/script @@ -8,5 +8,5 @@ trap cleanup EXIT # The direct engine puts ALL_PRIVILEGES in both the Add and Remove lists in the PATCH request, # which the backend rejects with "Duplicate privileges to add and delete". -$CLI bundle deploy > out.deploy.$DATABRICKS_BUNDLE_ENGINE.txt 2>&1 || true +$CLI bundle deploy print_requests.py --get //permissions --keep > out.requests.$DATABRICKS_BUNDLE_ENGINE.txt From c4abcccc2892e718560376f0d74ba14ed77f7a1d Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Mon, 23 Mar 2026 09:57:40 +0100 Subject: [PATCH 08/14] use json plan --- .../duplicate_principals/out.plan.direct.json | 81 +++++++++++++++++++ .../duplicate_principals/out.plan.direct.txt | 5 -- .../out.plan.terraform.json | 11 +++ .../out.plan.terraform.txt | 3 - .../schemas/duplicate_principals/output.txt | 2 + .../schemas/duplicate_principals/script | 2 +- 6 files changed, 95 insertions(+), 9 deletions(-) create mode 100644 acceptance/bundle/resources/grants/schemas/duplicate_principals/out.plan.direct.json delete mode 100644 acceptance/bundle/resources/grants/schemas/duplicate_principals/out.plan.direct.txt create mode 100644 acceptance/bundle/resources/grants/schemas/duplicate_principals/out.plan.terraform.json delete mode 100644 acceptance/bundle/resources/grants/schemas/duplicate_principals/out.plan.terraform.txt diff --git a/acceptance/bundle/resources/grants/schemas/duplicate_principals/out.plan.direct.json b/acceptance/bundle/resources/grants/schemas/duplicate_principals/out.plan.direct.json new file mode 100644 index 0000000000..92a89aef1c --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/duplicate_principals/out.plan.direct.json @@ -0,0 +1,81 @@ +{ + "plan_version": 2, + "cli_version": "[DEV_VERSION]", + "lineage": "[UUID]", + "serial": 1, + "plan": { + "resources.schemas.apps_schema": { + "action": "skip", + "remote_state": { + "browse_only": false, + "catalog_name": "main", + "catalog_type": "MANAGED_CATALOG", + "created_at": [UNIX_TIME_MILLIS][0], + "created_by": "[USERNAME]", + "full_name": "main.schema_dup_grants_[UNIQUE_NAME]", + "name": "schema_dup_grants_[UNIQUE_NAME]", + "owner": "[USERNAME]", + "updated_at": [UNIX_TIME_MILLIS][0], + "updated_by": "[USERNAME]" + } + }, + "resources.schemas.apps_schema.grants": { + "depends_on": [ + { + "node": "resources.schemas.apps_schema", + "label": "${resources.schemas.apps_schema.id}" + } + ], + "action": "update", + "new_state": { + "value": { + "securable_type": "schema", + "full_name": "main.schema_dup_grants_[UNIQUE_NAME]", + "__embed__": [ + { + "principal": "deco-test-user@databricks.com", + "privileges": [ + "CREATE_TABLE" + ] + }, + { + "principal": "deco-test-user@databricks.com", + "privileges": [ + "CREATE_TABLE" + ] + } + ] + } + }, + "remote_state": { + "securable_type": "schema", + "full_name": "main.schema_dup_grants_[UNIQUE_NAME]", + "__embed__": [ + { + "principal": "deco-test-user@databricks.com", + "privileges": [ + "CREATE_TABLE" + ] + } + ] + }, + "changes": { + "[principal='deco-test-user@databricks.com']": { + "action": "update", + "old": { + "principal": "deco-test-user@databricks.com", + "privileges": [ + "CREATE_TABLE" + ] + }, + "new": { + "principal": "deco-test-user@databricks.com", + "privileges": [ + "CREATE_TABLE" + ] + } + } + } + } + } +} diff --git a/acceptance/bundle/resources/grants/schemas/duplicate_principals/out.plan.direct.txt b/acceptance/bundle/resources/grants/schemas/duplicate_principals/out.plan.direct.txt deleted file mode 100644 index 746092b5c2..0000000000 --- a/acceptance/bundle/resources/grants/schemas/duplicate_principals/out.plan.direct.txt +++ /dev/null @@ -1,5 +0,0 @@ - ->>> [CLI] bundle plan -update schemas.apps_schema.grants - -Plan: 0 to add, 1 to change, 0 to delete, 1 unchanged diff --git a/acceptance/bundle/resources/grants/schemas/duplicate_principals/out.plan.terraform.json b/acceptance/bundle/resources/grants/schemas/duplicate_principals/out.plan.terraform.json new file mode 100644 index 0000000000..2c8b806d1e --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/duplicate_principals/out.plan.terraform.json @@ -0,0 +1,11 @@ +{ + "cli_version": "[DEV_VERSION]", + "plan": { + "resources.schemas.apps_schema": { + "action": "skip" + }, + "resources.schemas.apps_schema.grants": { + "action": "skip" + } + } +} diff --git a/acceptance/bundle/resources/grants/schemas/duplicate_principals/out.plan.terraform.txt b/acceptance/bundle/resources/grants/schemas/duplicate_principals/out.plan.terraform.txt deleted file mode 100644 index 068a177d51..0000000000 --- a/acceptance/bundle/resources/grants/schemas/duplicate_principals/out.plan.terraform.txt +++ /dev/null @@ -1,3 +0,0 @@ - ->>> [CLI] bundle plan -Plan: 0 to add, 0 to change, 0 to delete, 2 unchanged diff --git a/acceptance/bundle/resources/grants/schemas/duplicate_principals/output.txt b/acceptance/bundle/resources/grants/schemas/duplicate_principals/output.txt index a537487dbd..3b1393adf5 100644 --- a/acceptance/bundle/resources/grants/schemas/duplicate_principals/output.txt +++ b/acceptance/bundle/resources/grants/schemas/duplicate_principals/output.txt @@ -5,6 +5,8 @@ Deploying resources... Updating deployment state... Deployment complete! +>>> [CLI] bundle plan -o json + >>> [CLI] bundle destroy --auto-approve The following resources will be deleted: delete resources.schemas.apps_schema diff --git a/acceptance/bundle/resources/grants/schemas/duplicate_principals/script b/acceptance/bundle/resources/grants/schemas/duplicate_principals/script index 2929bacd17..af3ef3b137 100644 --- a/acceptance/bundle/resources/grants/schemas/duplicate_principals/script +++ b/acceptance/bundle/resources/grants/schemas/duplicate_principals/script @@ -9,4 +9,4 @@ trap cleanup EXIT # Same principal listed twice with the same privilege. trace $CLI bundle deploy print_requests.py --get //permissions --keep > out.requests.$DATABRICKS_BUNDLE_ENGINE.txt -trace $CLI bundle plan > out.plan.$DATABRICKS_BUNDLE_ENGINE.txt 2>&1 +trace $CLI bundle plan -o json > out.plan.$DATABRICKS_BUNDLE_ENGINE.json From ebb2fd2ce0e70b136e5a79fcebd02c11307f583a Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Mon, 23 Mar 2026 10:19:32 +0100 Subject: [PATCH 09/14] Add MergeGrants mutator to deduplicate grants during initialization Grants with duplicate principals or duplicate privileges were only deduplicated at deploy time in applyGrants(), causing the plan to report spurious changes. Move deduplication to the initialize phase as a normalize mutator, consistent with MergeJobClusters et al. Co-authored-by: Isaac --- .../duplicate_principals/out.plan.direct.json | 39 +---- .../duplicate_privileges/out.plan.direct.txt | 4 +- .../mutator/resourcemutator/merge_grants.go | 96 ++++++++++++ .../resourcemutator/merge_grants_test.go | 147 ++++++++++++++++++ .../resourcemutator/resource_mutator.go | 4 + 5 files changed, 249 insertions(+), 41 deletions(-) create mode 100644 bundle/config/mutator/resourcemutator/merge_grants.go create mode 100644 bundle/config/mutator/resourcemutator/merge_grants_test.go diff --git a/acceptance/bundle/resources/grants/schemas/duplicate_principals/out.plan.direct.json b/acceptance/bundle/resources/grants/schemas/duplicate_principals/out.plan.direct.json index 92a89aef1c..ce43b2a4d3 100644 --- a/acceptance/bundle/resources/grants/schemas/duplicate_principals/out.plan.direct.json +++ b/acceptance/bundle/resources/grants/schemas/duplicate_principals/out.plan.direct.json @@ -26,27 +26,7 @@ "label": "${resources.schemas.apps_schema.id}" } ], - "action": "update", - "new_state": { - "value": { - "securable_type": "schema", - "full_name": "main.schema_dup_grants_[UNIQUE_NAME]", - "__embed__": [ - { - "principal": "deco-test-user@databricks.com", - "privileges": [ - "CREATE_TABLE" - ] - }, - { - "principal": "deco-test-user@databricks.com", - "privileges": [ - "CREATE_TABLE" - ] - } - ] - } - }, + "action": "skip", "remote_state": { "securable_type": "schema", "full_name": "main.schema_dup_grants_[UNIQUE_NAME]", @@ -58,23 +38,6 @@ ] } ] - }, - "changes": { - "[principal='deco-test-user@databricks.com']": { - "action": "update", - "old": { - "principal": "deco-test-user@databricks.com", - "privileges": [ - "CREATE_TABLE" - ] - }, - "new": { - "principal": "deco-test-user@databricks.com", - "privileges": [ - "CREATE_TABLE" - ] - } - } } } } diff --git a/acceptance/bundle/resources/grants/schemas/duplicate_privileges/out.plan.direct.txt b/acceptance/bundle/resources/grants/schemas/duplicate_privileges/out.plan.direct.txt index 746092b5c2..068a177d51 100644 --- a/acceptance/bundle/resources/grants/schemas/duplicate_privileges/out.plan.direct.txt +++ b/acceptance/bundle/resources/grants/schemas/duplicate_privileges/out.plan.direct.txt @@ -1,5 +1,3 @@ >>> [CLI] bundle plan -update schemas.apps_schema.grants - -Plan: 0 to add, 1 to change, 0 to delete, 1 unchanged +Plan: 0 to add, 0 to change, 0 to delete, 2 unchanged diff --git a/bundle/config/mutator/resourcemutator/merge_grants.go b/bundle/config/mutator/resourcemutator/merge_grants.go new file mode 100644 index 0000000000..f517329c0b --- /dev/null +++ b/bundle/config/mutator/resourcemutator/merge_grants.go @@ -0,0 +1,96 @@ +package resourcemutator + +import ( + "context" + + "github.com/databricks/cli/bundle" + "github.com/databricks/cli/libs/diag" + "github.com/databricks/cli/libs/dyn" + "github.com/databricks/cli/libs/dyn/merge" +) + +// Resource types that support grants. +var grantResourceTypes = []string{ + "catalogs", + "schemas", + "external_locations", + "volumes", + "registered_models", +} + +type mergeGrants struct{} + +// MergeGrants returns a mutator that deduplicates grant entries. +// It merges entries with the same principal and deduplicates privileges. +func MergeGrants() bundle.Mutator { + return &mergeGrants{} +} + +func (m *mergeGrants) Name() string { + return "MergeGrants" +} + +func (m *mergeGrants) principalString(v dyn.Value) string { + switch v.Kind() { + case dyn.KindInvalid, dyn.KindNil: + return "" + case dyn.KindString: + return v.MustString() + default: + panic("principal must be a string") + } +} + +func (m *mergeGrants) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics { + err := b.Config.Mutate(func(v dyn.Value) (dyn.Value, error) { + if v.Kind() == dyn.KindNil { + return v, nil + } + + for _, resourceType := range grantResourceTypes { + var mapErr error + v, mapErr = dyn.Map(v, "resources."+resourceType, dyn.Foreach(func(_ dyn.Path, resource dyn.Value) (dyn.Value, error) { + // Merge grant entries by principal. This concatenates privileges + // for entries with the same principal via the standard merge semantics. + resource, err := dyn.Map(resource, "grants", merge.ElementsByKey("principal", m.principalString)) + if err != nil { + return resource, err + } + + // Deduplicate privileges within each grant entry. + return dyn.Map(resource, "grants", dyn.Foreach(func(_ dyn.Path, grant dyn.Value) (dyn.Value, error) { + return dyn.Map(grant, "privileges", deduplicateSequence) + })) + })) + if mapErr != nil { + return v, mapErr + } + } + + return v, nil + }) + + return diag.FromErr(err) +} + +// deduplicateSequence removes duplicate values from a dyn sequence, +// preserving the order of first appearance. +func deduplicateSequence(_ dyn.Path, v dyn.Value) (dyn.Value, error) { + elements, ok := v.AsSequence() + if !ok { + return v, nil + } + + seen := make(map[string]bool, len(elements)) + out := make([]dyn.Value, 0, len(elements)) + for _, elem := range elements { + key := elem.MustString() + if seen[key] { + continue + } + seen[key] = true + out = append(out, elem) + } + + return dyn.NewValue(out, v.Locations()), nil +} diff --git a/bundle/config/mutator/resourcemutator/merge_grants_test.go b/bundle/config/mutator/resourcemutator/merge_grants_test.go new file mode 100644 index 0000000000..8f3bc23c9e --- /dev/null +++ b/bundle/config/mutator/resourcemutator/merge_grants_test.go @@ -0,0 +1,147 @@ +package resourcemutator_test + +import ( + "testing" + + "github.com/databricks/cli/bundle" + "github.com/databricks/cli/bundle/config" + "github.com/databricks/cli/bundle/config/mutator/resourcemutator" + "github.com/databricks/cli/bundle/config/resources" + "github.com/databricks/databricks-sdk-go/service/catalog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMergeGrantsDuplicatePrincipals(t *testing.T) { + b := &bundle.Bundle{ + Config: config.Root{ + Resources: config.Resources{ + Schemas: map[string]*resources.Schema{ + "my_schema": { + Grants: []catalog.PrivilegeAssignment{ + { + Principal: "user@example.com", + Privileges: []catalog.Privilege{"CREATE_TABLE"}, + }, + { + Principal: "user@example.com", + Privileges: []catalog.Privilege{"CREATE_TABLE"}, + }, + }, + }, + }, + }, + }, + } + + diags := bundle.Apply(t.Context(), b, resourcemutator.MergeGrants()) + require.NoError(t, diags.Error()) + + grants := b.Config.Resources.Schemas["my_schema"].Grants + assert.Len(t, grants, 1) + assert.Equal(t, "user@example.com", grants[0].Principal) + assert.Equal(t, []catalog.Privilege{"CREATE_TABLE"}, grants[0].Privileges) +} + +func TestMergeGrantsDuplicatePrivileges(t *testing.T) { + b := &bundle.Bundle{ + Config: config.Root{ + Resources: config.Resources{ + Schemas: map[string]*resources.Schema{ + "my_schema": { + Grants: []catalog.PrivilegeAssignment{ + { + Principal: "user@example.com", + Privileges: []catalog.Privilege{"CREATE_TABLE", "CREATE_TABLE"}, + }, + }, + }, + }, + }, + }, + } + + diags := bundle.Apply(t.Context(), b, resourcemutator.MergeGrants()) + require.NoError(t, diags.Error()) + + grants := b.Config.Resources.Schemas["my_schema"].Grants + assert.Len(t, grants, 1) + assert.Equal(t, []catalog.Privilege{"CREATE_TABLE"}, grants[0].Privileges) +} + +func TestMergeGrantsMergesDifferentPrivileges(t *testing.T) { + b := &bundle.Bundle{ + Config: config.Root{ + Resources: config.Resources{ + Catalogs: map[string]*resources.Catalog{ + "my_catalog": { + Grants: []catalog.PrivilegeAssignment{ + { + Principal: "user@example.com", + Privileges: []catalog.Privilege{"USE_CATALOG"}, + }, + { + Principal: "user@example.com", + Privileges: []catalog.Privilege{"CREATE_SCHEMA"}, + }, + }, + }, + }, + }, + }, + } + + diags := bundle.Apply(t.Context(), b, resourcemutator.MergeGrants()) + require.NoError(t, diags.Error()) + + grants := b.Config.Resources.Catalogs["my_catalog"].Grants + assert.Len(t, grants, 1) + assert.Equal(t, "user@example.com", grants[0].Principal) + assert.Equal(t, []catalog.Privilege{"USE_CATALOG", "CREATE_SCHEMA"}, grants[0].Privileges) +} + +func TestMergeGrantsPreservesDistinctPrincipals(t *testing.T) { + b := &bundle.Bundle{ + Config: config.Root{ + Resources: config.Resources{ + Volumes: map[string]*resources.Volume{ + "my_volume": { + Grants: []catalog.PrivilegeAssignment{ + { + Principal: "user1@example.com", + Privileges: []catalog.Privilege{"READ_VOLUME"}, + }, + { + Principal: "user2@example.com", + Privileges: []catalog.Privilege{"WRITE_VOLUME"}, + }, + }, + }, + }, + }, + }, + } + + diags := bundle.Apply(t.Context(), b, resourcemutator.MergeGrants()) + require.NoError(t, diags.Error()) + + grants := b.Config.Resources.Volumes["my_volume"].Grants + assert.Len(t, grants, 2) + assert.Equal(t, "user1@example.com", grants[0].Principal) + assert.Equal(t, "user2@example.com", grants[1].Principal) +} + +func TestMergeGrantsNoGrants(t *testing.T) { + b := &bundle.Bundle{ + Config: config.Root{ + Resources: config.Resources{ + Schemas: map[string]*resources.Schema{ + "my_schema": {}, + }, + }, + }, + } + + diags := bundle.Apply(t.Context(), b, resourcemutator.MergeGrants()) + require.NoError(t, diags.Error()) +} diff --git a/bundle/config/mutator/resourcemutator/resource_mutator.go b/bundle/config/mutator/resourcemutator/resource_mutator.go index 1fb2626c6a..87069d6f84 100644 --- a/bundle/config/mutator/resourcemutator/resource_mutator.go +++ b/bundle/config/mutator/resourcemutator/resource_mutator.go @@ -167,6 +167,10 @@ func applyNormalizeMutators(ctx context.Context, b *bundle.Bundle) { // Updates (dynamic): resources.apps.*.resources (merges app resources with the same name) MergeApps(), + // Reads (dynamic): resources.{catalogs,schemas,external_locations,volumes,registered_models}.*.grants + // Updates (dynamic): same paths — merges grant entries by principal and deduplicates privileges + MergeGrants(), + // Reads (typed): resources.pipelines.*.{catalog,schema,target}, resources.volumes.*.{catalog_name,schema_name} (checks for schema references) // Updates (typed): resources.pipelines.*.{schema,target}, resources.volumes.*.schema_name (converts implicit schema references to explicit ${resources.schemas..name} syntax) // Translates implicit schema references in DLT pipelines or UC Volumes to explicit syntax to capture dependencies From 190b3b2d5e9a2e4fc0fab5f46f205237fdf0729d Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Mon, 23 Mar 2026 10:29:28 +0100 Subject: [PATCH 10/14] Remove redundant mergeGrantAssignments from applyGrants Deduplication is now handled by the MergeGrants mutator during initialization, so applyGrants can iterate directly. Co-authored-by: Isaac --- bundle/direct/dresources/grants.go | 36 +----------------------------- 1 file changed, 1 insertion(+), 35 deletions(-) diff --git a/bundle/direct/dresources/grants.go b/bundle/direct/dresources/grants.go index b6ae1e5cc1..e16abda191 100644 --- a/bundle/direct/dresources/grants.go +++ b/bundle/direct/dresources/grants.go @@ -130,11 +130,8 @@ func (r *ResourceGrants) applyGrants(ctx context.Context, state *GrantsState) er return errors.New("internal error: grants full_name must be resolved before deployment") } - // Merge privileges by principal and deduplicate. - merged := mergeGrantAssignments(state.EmbeddedSlice) - var changes []catalog.PermissionsChange - for _, ga := range merged { + for _, ga := range state.EmbeddedSlice { change := catalog.PermissionsChange{ Principal: ga.Principal, Add: ga.Privileges, @@ -155,37 +152,6 @@ func (r *ResourceGrants) applyGrants(ctx context.Context, state *GrantsState) er return err } -// mergeGrantAssignments consolidates multiple entries for the same principal -// into a single entry with deduplicated, sorted privileges. -func mergeGrantAssignments(assignments []catalog.PrivilegeAssignment) []catalog.PrivilegeAssignment { - seen := map[string]map[catalog.Privilege]bool{} - var order []string - - for _, a := range assignments { - if seen[a.Principal] == nil { - seen[a.Principal] = map[catalog.Privilege]bool{} - order = append(order, a.Principal) - } - for _, p := range a.Privileges { - seen[a.Principal][p] = true - } - } - - result := make([]catalog.PrivilegeAssignment, 0, len(order)) - for _, principal := range order { - privs := make([]catalog.Privilege, 0, len(seen[principal])) - for p := range seen[principal] { - privs = append(privs, p) - } - slices.Sort(privs) - result = append(result, catalog.PrivilegeAssignment{ - Principal: principal, - Privileges: privs, - }) - } - return result -} - func (r *ResourceGrants) listGrants(ctx context.Context, securableType, fullName string) ([]catalog.PrivilegeAssignment, error) { var assignments []catalog.PrivilegeAssignment pageToken := "" From 11b0db8c4ee4134089a1bab78e2c1e676028c671 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Mon, 23 Mar 2026 10:33:10 +0100 Subject: [PATCH 11/14] Simplify MergeGrants: inline principalString, avoid panics Use AsString instead of MustString to avoid panics on unexpected config values. Co-authored-by: Isaac --- .../mutator/resourcemutator/merge_grants.go | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/bundle/config/mutator/resourcemutator/merge_grants.go b/bundle/config/mutator/resourcemutator/merge_grants.go index f517329c0b..1f8d033747 100644 --- a/bundle/config/mutator/resourcemutator/merge_grants.go +++ b/bundle/config/mutator/resourcemutator/merge_grants.go @@ -30,17 +30,6 @@ func (m *mergeGrants) Name() string { return "MergeGrants" } -func (m *mergeGrants) principalString(v dyn.Value) string { - switch v.Kind() { - case dyn.KindInvalid, dyn.KindNil: - return "" - case dyn.KindString: - return v.MustString() - default: - panic("principal must be a string") - } -} - func (m *mergeGrants) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics { err := b.Config.Mutate(func(v dyn.Value) (dyn.Value, error) { if v.Kind() == dyn.KindNil { @@ -52,7 +41,10 @@ func (m *mergeGrants) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnost v, mapErr = dyn.Map(v, "resources."+resourceType, dyn.Foreach(func(_ dyn.Path, resource dyn.Value) (dyn.Value, error) { // Merge grant entries by principal. This concatenates privileges // for entries with the same principal via the standard merge semantics. - resource, err := dyn.Map(resource, "grants", merge.ElementsByKey("principal", m.principalString)) + resource, err := dyn.Map(resource, "grants", merge.ElementsByKey("principal", func(v dyn.Value) string { + s, _ := v.AsString() + return s + })) if err != nil { return resource, err } @@ -84,7 +76,7 @@ func deduplicateSequence(_ dyn.Path, v dyn.Value) (dyn.Value, error) { seen := make(map[string]bool, len(elements)) out := make([]dyn.Value, 0, len(elements)) for _, elem := range elements { - key := elem.MustString() + key, _ := elem.AsString() if seen[key] { continue } From 39aa69dbb043d0251855f84705b5853f88d1cc97 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Mon, 23 Mar 2026 10:39:39 +0100 Subject: [PATCH 12/14] lint fix --- bundle/direct/dresources/grants.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bundle/direct/dresources/grants.go b/bundle/direct/dresources/grants.go index e16abda191..3172ce55e3 100644 --- a/bundle/direct/dresources/grants.go +++ b/bundle/direct/dresources/grants.go @@ -133,8 +133,10 @@ func (r *ResourceGrants) applyGrants(ctx context.Context, state *GrantsState) er var changes []catalog.PermissionsChange for _, ga := range state.EmbeddedSlice { change := catalog.PermissionsChange{ - Principal: ga.Principal, - Add: ga.Privileges, + Principal: ga.Principal, + Add: ga.Privileges, + Remove: nil, + ForceSendFields: nil, } // Remove all other privileges unless ALL_PRIVILEGES is being granted // (it would conflict with appearing in both Add and Remove). From 16481c8470b0f3220e232099bfbe05309882c9cc Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Mon, 23 Mar 2026 10:39:52 +0100 Subject: [PATCH 13/14] remove mutator unit test --- .../resourcemutator/merge_grants_test.go | 147 ------------------ 1 file changed, 147 deletions(-) delete mode 100644 bundle/config/mutator/resourcemutator/merge_grants_test.go diff --git a/bundle/config/mutator/resourcemutator/merge_grants_test.go b/bundle/config/mutator/resourcemutator/merge_grants_test.go deleted file mode 100644 index 8f3bc23c9e..0000000000 --- a/bundle/config/mutator/resourcemutator/merge_grants_test.go +++ /dev/null @@ -1,147 +0,0 @@ -package resourcemutator_test - -import ( - "testing" - - "github.com/databricks/cli/bundle" - "github.com/databricks/cli/bundle/config" - "github.com/databricks/cli/bundle/config/mutator/resourcemutator" - "github.com/databricks/cli/bundle/config/resources" - "github.com/databricks/databricks-sdk-go/service/catalog" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestMergeGrantsDuplicatePrincipals(t *testing.T) { - b := &bundle.Bundle{ - Config: config.Root{ - Resources: config.Resources{ - Schemas: map[string]*resources.Schema{ - "my_schema": { - Grants: []catalog.PrivilegeAssignment{ - { - Principal: "user@example.com", - Privileges: []catalog.Privilege{"CREATE_TABLE"}, - }, - { - Principal: "user@example.com", - Privileges: []catalog.Privilege{"CREATE_TABLE"}, - }, - }, - }, - }, - }, - }, - } - - diags := bundle.Apply(t.Context(), b, resourcemutator.MergeGrants()) - require.NoError(t, diags.Error()) - - grants := b.Config.Resources.Schemas["my_schema"].Grants - assert.Len(t, grants, 1) - assert.Equal(t, "user@example.com", grants[0].Principal) - assert.Equal(t, []catalog.Privilege{"CREATE_TABLE"}, grants[0].Privileges) -} - -func TestMergeGrantsDuplicatePrivileges(t *testing.T) { - b := &bundle.Bundle{ - Config: config.Root{ - Resources: config.Resources{ - Schemas: map[string]*resources.Schema{ - "my_schema": { - Grants: []catalog.PrivilegeAssignment{ - { - Principal: "user@example.com", - Privileges: []catalog.Privilege{"CREATE_TABLE", "CREATE_TABLE"}, - }, - }, - }, - }, - }, - }, - } - - diags := bundle.Apply(t.Context(), b, resourcemutator.MergeGrants()) - require.NoError(t, diags.Error()) - - grants := b.Config.Resources.Schemas["my_schema"].Grants - assert.Len(t, grants, 1) - assert.Equal(t, []catalog.Privilege{"CREATE_TABLE"}, grants[0].Privileges) -} - -func TestMergeGrantsMergesDifferentPrivileges(t *testing.T) { - b := &bundle.Bundle{ - Config: config.Root{ - Resources: config.Resources{ - Catalogs: map[string]*resources.Catalog{ - "my_catalog": { - Grants: []catalog.PrivilegeAssignment{ - { - Principal: "user@example.com", - Privileges: []catalog.Privilege{"USE_CATALOG"}, - }, - { - Principal: "user@example.com", - Privileges: []catalog.Privilege{"CREATE_SCHEMA"}, - }, - }, - }, - }, - }, - }, - } - - diags := bundle.Apply(t.Context(), b, resourcemutator.MergeGrants()) - require.NoError(t, diags.Error()) - - grants := b.Config.Resources.Catalogs["my_catalog"].Grants - assert.Len(t, grants, 1) - assert.Equal(t, "user@example.com", grants[0].Principal) - assert.Equal(t, []catalog.Privilege{"USE_CATALOG", "CREATE_SCHEMA"}, grants[0].Privileges) -} - -func TestMergeGrantsPreservesDistinctPrincipals(t *testing.T) { - b := &bundle.Bundle{ - Config: config.Root{ - Resources: config.Resources{ - Volumes: map[string]*resources.Volume{ - "my_volume": { - Grants: []catalog.PrivilegeAssignment{ - { - Principal: "user1@example.com", - Privileges: []catalog.Privilege{"READ_VOLUME"}, - }, - { - Principal: "user2@example.com", - Privileges: []catalog.Privilege{"WRITE_VOLUME"}, - }, - }, - }, - }, - }, - }, - } - - diags := bundle.Apply(t.Context(), b, resourcemutator.MergeGrants()) - require.NoError(t, diags.Error()) - - grants := b.Config.Resources.Volumes["my_volume"].Grants - assert.Len(t, grants, 2) - assert.Equal(t, "user1@example.com", grants[0].Principal) - assert.Equal(t, "user2@example.com", grants[1].Principal) -} - -func TestMergeGrantsNoGrants(t *testing.T) { - b := &bundle.Bundle{ - Config: config.Root{ - Resources: config.Resources{ - Schemas: map[string]*resources.Schema{ - "my_schema": {}, - }, - }, - }, - } - - diags := bundle.Apply(t.Context(), b, resourcemutator.MergeGrants()) - require.NoError(t, diags.Error()) -} From 0f8d5e69f8131fea028f7ede3d401fdfe9e99e29 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Mon, 23 Mar 2026 10:46:35 +0100 Subject: [PATCH 14/14] Update NEXT_CHANGELOG with grants fixes Co-authored-by: Isaac --- NEXT_CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index 6b2aed9c47..42dc64d7d3 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -8,6 +8,8 @@ ### Bundles * engine/direct: Fix drift in grants resource due to privilege reordering ([#4794](https://github.com/databricks/cli/pull/4794)) +* engine/direct: Fix 400 error when deploying grants with ALL_PRIVILEGES ([#4801](https://github.com/databricks/cli/pull/4801)) +* Deduplicate grant entries with duplicate principals or privileges during initialization ([#4801](https://github.com/databricks/cli/pull/4801)) ### Dependency updates