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 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.requests.direct.txt b/acceptance/bundle/resources/grants/schemas/all_privileges/out.requests.direct.txt new file mode 100644 index 0000000000..d7cef386ad --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/all_privileges/out.requests.direct.txt @@ -0,0 +1,14 @@ +{ + "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" + } + ] + } +} 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/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..e88c30715f --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/all_privileges/output.txt @@ -0,0 +1,16 @@ +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/all_privileges/script b/acceptance/bundle/resources/grants/schemas/all_privileges/script new file mode 100644 index 0000000000..e8f6e243dd --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/all_privileges/script @@ -0,0 +1,12 @@ +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 +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 new file mode 100644 index 0000000000..159efe0269 --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/all_privileges/test.toml @@ -0,0 +1 @@ +RecordRequests = true 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.json b/acceptance/bundle/resources/grants/schemas/duplicate_principals/out.plan.direct.json new file mode 100644 index 0000000000..ce43b2a4d3 --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/duplicate_principals/out.plan.direct.json @@ -0,0 +1,44 @@ +{ + "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": "skip", + "remote_state": { + "securable_type": "schema", + "full_name": "main.schema_dup_grants_[UNIQUE_NAME]", + "__embed__": [ + { + "principal": "deco-test-user@databricks.com", + "privileges": [ + "CREATE_TABLE" + ] + } + ] + } + } + } +} 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.requests.direct.txt b/acceptance/bundle/resources/grants/schemas/duplicate_principals/out.requests.direct.txt new file mode 100644 index 0000000000..a0d3144160 --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/duplicate_principals/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": [ + "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/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..3b1393adf5 --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/duplicate_principals/output.txt @@ -0,0 +1,20 @@ + +>>> [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 plan -o json + +>>> [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..af3ef3b137 --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/duplicate_principals/script @@ -0,0 +1,12 @@ +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 -o json > out.plan.$DATABRICKS_BUNDLE_ENGINE.json 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..159efe0269 --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/duplicate_principals/test.toml @@ -0,0 +1 @@ +RecordRequests = true 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..068a177d51 --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/duplicate_privileges/out.plan.direct.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.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.requests.direct.txt b/acceptance/bundle/resources/grants/schemas/duplicate_privileges/out.requests.direct.txt new file mode 100644 index 0000000000..a0d3144160 --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/duplicate_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": [ + "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/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..6118156ba5 --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/duplicate_privileges/script @@ -0,0 +1,12 @@ +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 new file mode 100644 index 0000000000..159efe0269 --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/duplicate_privileges/test.toml @@ -0,0 +1 @@ +RecordRequests = true diff --git a/bundle/config/mutator/resourcemutator/merge_grants.go b/bundle/config/mutator/resourcemutator/merge_grants.go new file mode 100644 index 0000000000..1f8d033747 --- /dev/null +++ b/bundle/config/mutator/resourcemutator/merge_grants.go @@ -0,0 +1,88 @@ +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) 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", func(v dyn.Value) string { + s, _ := v.AsString() + return s + })) + 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.AsString() + 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/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 diff --git a/bundle/direct/dresources/grants.go b/bundle/direct/dresources/grants.go index 00c8281aa3..3172ce55e3 100644 --- a/bundle/direct/dresources/grants.go +++ b/bundle/direct/dresources/grants.go @@ -131,15 +131,19 @@ func (r *ResourceGrants) applyGrants(ctx context.Context, state *GrantsState) er } var changes []catalog.PermissionsChange - - // 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}, + for _, ga := range state.EmbeddedSlice { + change := catalog.PermissionsChange{ + 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). + 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{ 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 == "" {