From 8ead22b438f328b3032f3d1e0111f503c803e7a0 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Thu, 2 Jul 2026 18:12:19 +0200 Subject: [PATCH 1/6] Reproduce #5682: permissions added by Python mutator lack IS_OWNER Permissions added to an existing resource by a PyDABs job/pipeline mutator go through NormalizeResources (UpdatedResources path), which does not run FixPermissions. The deploying user is therefore never added as IS_OWNER, and the direct engine PUTs an ownerless ACL that the Permissions API rejects with "The pipeline must have exactly one owner". The terraform provider re-injects the owner at PUT time, so it succeeds - hence the terraform-vs-direct divergence. Model the real backend rule (jobs and pipelines require exactly one owner; zero or two both fail) in the testserver, and add an acceptance test that deploys and records the per-engine outcome. Co-authored-by: Isaac --- .../databricks.yml | 11 +++++++++++ .../mutators.py | 17 +++++++++++++++++ .../out.deploy.direct.txt | 14 ++++++++++++++ .../out.deploy.terraform.txt | 6 ++++++ .../out.test.toml | 4 ++++ .../mutator-permissions-owner-5682/output.txt | 8 ++++++++ .../mutator-permissions-owner-5682/script | 11 +++++++++++ libs/testserver/permissions.go | 19 +++++++++---------- 8 files changed, 80 insertions(+), 10 deletions(-) create mode 100644 acceptance/bundle/python/mutator-permissions-owner-5682/databricks.yml create mode 100644 acceptance/bundle/python/mutator-permissions-owner-5682/mutators.py create mode 100644 acceptance/bundle/python/mutator-permissions-owner-5682/out.deploy.direct.txt create mode 100644 acceptance/bundle/python/mutator-permissions-owner-5682/out.deploy.terraform.txt create mode 100644 acceptance/bundle/python/mutator-permissions-owner-5682/out.test.toml create mode 100644 acceptance/bundle/python/mutator-permissions-owner-5682/output.txt create mode 100644 acceptance/bundle/python/mutator-permissions-owner-5682/script diff --git a/acceptance/bundle/python/mutator-permissions-owner-5682/databricks.yml b/acceptance/bundle/python/mutator-permissions-owner-5682/databricks.yml new file mode 100644 index 00000000000..97d0a682c60 --- /dev/null +++ b/acceptance/bundle/python/mutator-permissions-owner-5682/databricks.yml @@ -0,0 +1,11 @@ +bundle: + name: my_project + +python: + mutators: + - "mutators:add_pipeline_permission" + +resources: + pipelines: + my_pipeline: + name: my_pipeline diff --git a/acceptance/bundle/python/mutator-permissions-owner-5682/mutators.py b/acceptance/bundle/python/mutator-permissions-owner-5682/mutators.py new file mode 100644 index 00000000000..2922652164b --- /dev/null +++ b/acceptance/bundle/python/mutator-permissions-owner-5682/mutators.py @@ -0,0 +1,17 @@ +from dataclasses import replace +from databricks.bundles.pipelines import Pipeline, PipelinePermission +from databricks.bundles.core import pipeline_mutator, Bundle + + +# Reproduces https://github.com/databricks/cli/issues/5682. +# Adds a permission to a pipeline that is already defined in YAML. Resources updated +# by a PythonMutator go through NormalizeResources, which does NOT run FixPermissions, +# so the deploying user is never added as IS_OWNER. The resulting ownerless permissions +# PUT is rejected by the backend with "The pipeline must have exactly one owner". +@pipeline_mutator +def add_pipeline_permission(bundle: Bundle, pipeline: Pipeline) -> Pipeline: + permissions = [ + *pipeline.permissions, + PipelinePermission.from_dict({"group_name": "some-group", "level": "CAN_RUN"}), + ] + return replace(pipeline, permissions=permissions) diff --git a/acceptance/bundle/python/mutator-permissions-owner-5682/out.deploy.direct.txt b/acceptance/bundle/python/mutator-permissions-owner-5682/out.deploy.direct.txt new file mode 100644 index 00000000000..d4d682c8d22 --- /dev/null +++ b/acceptance/bundle/python/mutator-permissions-owner-5682/out.deploy.direct.txt @@ -0,0 +1,14 @@ + +>>> errcode uv run [UV_ARGS] -q [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/my_project/default/files... +Deploying resources... +Error: cannot create resources.pipelines.my_pipeline.permissions: The pipeline must have exactly one owner. (400 ) + +Endpoint: PUT [DATABRICKS_URL]/api/2.0/permissions/pipelines/[UUID] +HTTP Status: 400 Bad Request +API error_code: +API message: The pipeline must have exactly one owner. + +Updating deployment state... + +Exit code: 1 diff --git a/acceptance/bundle/python/mutator-permissions-owner-5682/out.deploy.terraform.txt b/acceptance/bundle/python/mutator-permissions-owner-5682/out.deploy.terraform.txt new file mode 100644 index 00000000000..dc50dd4e6fa --- /dev/null +++ b/acceptance/bundle/python/mutator-permissions-owner-5682/out.deploy.terraform.txt @@ -0,0 +1,6 @@ + +>>> errcode uv run [UV_ARGS] -q [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/my_project/default/files... +Deploying resources... +Updating deployment state... +Deployment complete! diff --git a/acceptance/bundle/python/mutator-permissions-owner-5682/out.test.toml b/acceptance/bundle/python/mutator-permissions-owner-5682/out.test.toml new file mode 100644 index 00000000000..0969b3f3733 --- /dev/null +++ b/acceptance/bundle/python/mutator-permissions-owner-5682/out.test.toml @@ -0,0 +1,4 @@ +Local = true +Cloud = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] +EnvMatrix.PYDAB_VERSION = ["0.266.0", "current"] diff --git a/acceptance/bundle/python/mutator-permissions-owner-5682/output.txt b/acceptance/bundle/python/mutator-permissions-owner-5682/output.txt new file mode 100644 index 00000000000..634442d6788 --- /dev/null +++ b/acceptance/bundle/python/mutator-permissions-owner-5682/output.txt @@ -0,0 +1,8 @@ + +>>> uv run [UV_ARGS] -q [CLI] bundle validate --output json +[ + { + "group_name": "some-group", + "level": "CAN_RUN" + } +] diff --git a/acceptance/bundle/python/mutator-permissions-owner-5682/script b/acceptance/bundle/python/mutator-permissions-owner-5682/script new file mode 100644 index 00000000000..01b118596d0 --- /dev/null +++ b/acceptance/bundle/python/mutator-permissions-owner-5682/script @@ -0,0 +1,11 @@ +# The Python mutator adds a permission but no owner. FixPermissions does not run on +# resources updated by PythonMutator, so no IS_OWNER is added for the current user. +trace uv run $UV_ARGS -q $CLI bundle validate --output json | \ + jq ".resources.pipelines.my_pipeline.permissions" + +# BUG #5682: the direct engine PUTs the ownerless ACL and the backend rejects it with +# "The pipeline must have exactly one owner". The terraform provider re-injects the +# owner at PUT time, so it succeeds. Deploy output therefore differs per engine. +trace errcode uv run $UV_ARGS -q $CLI bundle deploy &> out.deploy.$DATABRICKS_BUNDLE_ENGINE.txt + +rm -fr .databricks __pycache__ diff --git a/libs/testserver/permissions.go b/libs/testserver/permissions.go index 1270ad85720..a6610a8b091 100644 --- a/libs/testserver/permissions.go +++ b/libs/testserver/permissions.go @@ -314,25 +314,24 @@ func (s *FakeWorkspace) SetPermissions(req Request) any { }) } - // Validate job ownership requirements - if requestObjectType == "jobs" { - hasOwner := false + // Jobs and pipelines require exactly one owner. The real Permissions API rejects + // a PUT with zero owners OR more than one owner (verified against the backend); + // both cases return "The must have exactly one owner." + ownerNoun := map[string]string{"jobs": "job", "pipelines": "pipeline"}[requestObjectType] + if ownerNoun != "" { + owners := 0 for _, acl := range existingPermissions.AccessControlList { for _, perm := range acl.AllPermissions { if perm.PermissionLevel == "IS_OWNER" { - hasOwner = true - break + owners++ } } - if hasOwner { - break - } } - if !hasOwner { + if owners != 1 { return Response{ StatusCode: 400, - Body: map[string]string{"message": "The job must have exactly one owner."}, + Body: map[string]string{"message": "The " + ownerNoun + " must have exactly one owner."}, } } } From a72260444c6debb7202d669154885e6456894de8 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Fri, 3 Jul 2026 13:51:07 +0200 Subject: [PATCH 2/6] Make #5682 repro cloud-runnable and match backend error_code Convert the reproduction to a real deploy against a UC pipeline: notebook library, unique names via envsubst, a real grantee group on cloud, and cleanup. Add INVALID_PARAMETER_VALUE to the testserver's owner error to match the real Permissions API response shown in the issue. Co-authored-by: Isaac --- .../databricks.yml | 11 ---------- .../databricks.yml.tmpl | 21 +++++++++++++++++++ .../mutators.py | 12 ++++++----- .../mutator-permissions-owner-5682/nb.py | 1 + .../out.deploy.direct.txt | 6 +++--- .../out.deploy.terraform.txt | 2 +- .../out.destroy.direct.txt | 13 ++++++++++++ .../out.destroy.terraform.txt | 13 ++++++++++++ .../out.test.toml | 3 ++- .../mutator-permissions-owner-5682/output.txt | 5 +---- .../mutator-permissions-owner-5682/script | 19 ++++++++++++++--- .../mutator-permissions-owner-5682/test.toml | 2 ++ libs/testserver/permissions.go | 5 ++++- 13 files changed, 84 insertions(+), 29 deletions(-) delete mode 100644 acceptance/bundle/python/mutator-permissions-owner-5682/databricks.yml create mode 100644 acceptance/bundle/python/mutator-permissions-owner-5682/databricks.yml.tmpl create mode 100644 acceptance/bundle/python/mutator-permissions-owner-5682/nb.py create mode 100644 acceptance/bundle/python/mutator-permissions-owner-5682/out.destroy.direct.txt create mode 100644 acceptance/bundle/python/mutator-permissions-owner-5682/out.destroy.terraform.txt create mode 100644 acceptance/bundle/python/mutator-permissions-owner-5682/test.toml diff --git a/acceptance/bundle/python/mutator-permissions-owner-5682/databricks.yml b/acceptance/bundle/python/mutator-permissions-owner-5682/databricks.yml deleted file mode 100644 index 97d0a682c60..00000000000 --- a/acceptance/bundle/python/mutator-permissions-owner-5682/databricks.yml +++ /dev/null @@ -1,11 +0,0 @@ -bundle: - name: my_project - -python: - mutators: - - "mutators:add_pipeline_permission" - -resources: - pipelines: - my_pipeline: - name: my_pipeline diff --git a/acceptance/bundle/python/mutator-permissions-owner-5682/databricks.yml.tmpl b/acceptance/bundle/python/mutator-permissions-owner-5682/databricks.yml.tmpl new file mode 100644 index 00000000000..ac0754ae15b --- /dev/null +++ b/acceptance/bundle/python/mutator-permissions-owner-5682/databricks.yml.tmpl @@ -0,0 +1,21 @@ +bundle: + name: test-bundle-$UNIQUE_NAME + +variables: + grantee_group: + default: some-group + +python: + mutators: + - "mutators:add_pipeline_permission" + +resources: + pipelines: + my_pipeline: + name: test-pipeline-$UNIQUE_NAME + catalog: main + schema: default + serverless: true + libraries: + - notebook: + path: ./nb.py diff --git a/acceptance/bundle/python/mutator-permissions-owner-5682/mutators.py b/acceptance/bundle/python/mutator-permissions-owner-5682/mutators.py index 2922652164b..811a3291034 100644 --- a/acceptance/bundle/python/mutator-permissions-owner-5682/mutators.py +++ b/acceptance/bundle/python/mutator-permissions-owner-5682/mutators.py @@ -4,14 +4,16 @@ # Reproduces https://github.com/databricks/cli/issues/5682. -# Adds a permission to a pipeline that is already defined in YAML. Resources updated -# by a PythonMutator go through NormalizeResources, which does NOT run FixPermissions, -# so the deploying user is never added as IS_OWNER. The resulting ownerless permissions -# PUT is rejected by the backend with "The pipeline must have exactly one owner". +# Adds a permission to a pipeline that is already defined in YAML (mirrors the +# reporter's mutator, which reads a bundle variable). Resources updated by a +# PythonMutator go through NormalizeResources, which does NOT run FixPermissions, +# so the deploying user is never added as IS_OWNER. The resulting ownerless +# permissions PUT is rejected with "The pipeline must have exactly one owner". @pipeline_mutator def add_pipeline_permission(bundle: Bundle, pipeline: Pipeline) -> Pipeline: + group = bundle.resolve_variable(bundle.variables["grantee_group"]) permissions = [ *pipeline.permissions, - PipelinePermission.from_dict({"group_name": "some-group", "level": "CAN_RUN"}), + PipelinePermission.from_dict({"group_name": group, "level": "CAN_RUN"}), ] return replace(pipeline, permissions=permissions) diff --git a/acceptance/bundle/python/mutator-permissions-owner-5682/nb.py b/acceptance/bundle/python/mutator-permissions-owner-5682/nb.py new file mode 100644 index 00000000000..1645e04b1de --- /dev/null +++ b/acceptance/bundle/python/mutator-permissions-owner-5682/nb.py @@ -0,0 +1 @@ +# Databricks notebook source diff --git a/acceptance/bundle/python/mutator-permissions-owner-5682/out.deploy.direct.txt b/acceptance/bundle/python/mutator-permissions-owner-5682/out.deploy.direct.txt index d4d682c8d22..bbfa9db81f9 100644 --- a/acceptance/bundle/python/mutator-permissions-owner-5682/out.deploy.direct.txt +++ b/acceptance/bundle/python/mutator-permissions-owner-5682/out.deploy.direct.txt @@ -1,12 +1,12 @@ >>> errcode uv run [UV_ARGS] -q [CLI] bundle deploy -Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/my_project/default/files... +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle-[UNIQUE_NAME]/default/files... Deploying resources... -Error: cannot create resources.pipelines.my_pipeline.permissions: The pipeline must have exactly one owner. (400 ) +Error: cannot create resources.pipelines.my_pipeline.permissions: The pipeline must have exactly one owner. (400 INVALID_PARAMETER_VALUE) Endpoint: PUT [DATABRICKS_URL]/api/2.0/permissions/pipelines/[UUID] HTTP Status: 400 Bad Request -API error_code: +API error_code: INVALID_PARAMETER_VALUE API message: The pipeline must have exactly one owner. Updating deployment state... diff --git a/acceptance/bundle/python/mutator-permissions-owner-5682/out.deploy.terraform.txt b/acceptance/bundle/python/mutator-permissions-owner-5682/out.deploy.terraform.txt index dc50dd4e6fa..f78330641b5 100644 --- a/acceptance/bundle/python/mutator-permissions-owner-5682/out.deploy.terraform.txt +++ b/acceptance/bundle/python/mutator-permissions-owner-5682/out.deploy.terraform.txt @@ -1,6 +1,6 @@ >>> errcode uv run [UV_ARGS] -q [CLI] bundle deploy -Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/my_project/default/files... +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle-[UNIQUE_NAME]/default/files... Deploying resources... Updating deployment state... Deployment complete! diff --git a/acceptance/bundle/python/mutator-permissions-owner-5682/out.destroy.direct.txt b/acceptance/bundle/python/mutator-permissions-owner-5682/out.destroy.direct.txt new file mode 100644 index 00000000000..297e35e6514 --- /dev/null +++ b/acceptance/bundle/python/mutator-permissions-owner-5682/out.destroy.direct.txt @@ -0,0 +1,13 @@ + +>>> errcode uv run [UV_ARGS] -q [CLI] bundle destroy --auto-approve +The following resources will be deleted: + delete resources.pipelines.my_pipeline + +This action will result in the deletion of the following Lakeflow Spark Declarative Pipelines along with the +Streaming Tables (STs) and Materialized Views (MVs) managed by them: + delete resources.pipelines.my_pipeline + +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test-bundle-[UNIQUE_NAME]/default + +Deleting files... +Destroy complete! diff --git a/acceptance/bundle/python/mutator-permissions-owner-5682/out.destroy.terraform.txt b/acceptance/bundle/python/mutator-permissions-owner-5682/out.destroy.terraform.txt new file mode 100644 index 00000000000..297e35e6514 --- /dev/null +++ b/acceptance/bundle/python/mutator-permissions-owner-5682/out.destroy.terraform.txt @@ -0,0 +1,13 @@ + +>>> errcode uv run [UV_ARGS] -q [CLI] bundle destroy --auto-approve +The following resources will be deleted: + delete resources.pipelines.my_pipeline + +This action will result in the deletion of the following Lakeflow Spark Declarative Pipelines along with the +Streaming Tables (STs) and Materialized Views (MVs) managed by them: + delete resources.pipelines.my_pipeline + +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test-bundle-[UNIQUE_NAME]/default + +Deleting files... +Destroy complete! diff --git a/acceptance/bundle/python/mutator-permissions-owner-5682/out.test.toml b/acceptance/bundle/python/mutator-permissions-owner-5682/out.test.toml index 0969b3f3733..4c48a83f25b 100644 --- a/acceptance/bundle/python/mutator-permissions-owner-5682/out.test.toml +++ b/acceptance/bundle/python/mutator-permissions-owner-5682/out.test.toml @@ -1,4 +1,5 @@ Local = true -Cloud = false +Cloud = true +RequiresUnityCatalog = true EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] EnvMatrix.PYDAB_VERSION = ["0.266.0", "current"] diff --git a/acceptance/bundle/python/mutator-permissions-owner-5682/output.txt b/acceptance/bundle/python/mutator-permissions-owner-5682/output.txt index 634442d6788..24b9a2cb632 100644 --- a/acceptance/bundle/python/mutator-permissions-owner-5682/output.txt +++ b/acceptance/bundle/python/mutator-permissions-owner-5682/output.txt @@ -1,8 +1,5 @@ >>> uv run [UV_ARGS] -q [CLI] bundle validate --output json [ - { - "group_name": "some-group", - "level": "CAN_RUN" - } + "CAN_RUN" ] diff --git a/acceptance/bundle/python/mutator-permissions-owner-5682/script b/acceptance/bundle/python/mutator-permissions-owner-5682/script index 01b118596d0..9e7e6586963 100644 --- a/acceptance/bundle/python/mutator-permissions-owner-5682/script +++ b/acceptance/bundle/python/mutator-permissions-owner-5682/script @@ -1,11 +1,24 @@ +envsubst < databricks.yml.tmpl > databricks.yml + +if [ -n "$CLOUD_ENV" ]; then + # Unique group so parallel cloud runs don't collide; must exist for the PUT to + # reach the owner check rather than failing on an unknown principal. + export BUNDLE_VAR_grantee_group="dabs-5682-$UNIQUE_NAME" + $CLI groups create --display-name "$BUNDLE_VAR_grantee_group" &> /dev/null || true +fi + +cleanup() { + trace errcode uv run $UV_ARGS -q $CLI bundle destroy --auto-approve &> out.destroy.$DATABRICKS_BUNDLE_ENGINE.txt + rm -fr .databricks __pycache__ +} +trap cleanup EXIT + # The Python mutator adds a permission but no owner. FixPermissions does not run on # resources updated by PythonMutator, so no IS_OWNER is added for the current user. trace uv run $UV_ARGS -q $CLI bundle validate --output json | \ - jq ".resources.pipelines.my_pipeline.permissions" + jq ".resources.pipelines.my_pipeline.permissions | map(.level)" # BUG #5682: the direct engine PUTs the ownerless ACL and the backend rejects it with # "The pipeline must have exactly one owner". The terraform provider re-injects the # owner at PUT time, so it succeeds. Deploy output therefore differs per engine. trace errcode uv run $UV_ARGS -q $CLI bundle deploy &> out.deploy.$DATABRICKS_BUNDLE_ENGINE.txt - -rm -fr .databricks __pycache__ diff --git a/acceptance/bundle/python/mutator-permissions-owner-5682/test.toml b/acceptance/bundle/python/mutator-permissions-owner-5682/test.toml new file mode 100644 index 00000000000..476b9209ae6 --- /dev/null +++ b/acceptance/bundle/python/mutator-permissions-owner-5682/test.toml @@ -0,0 +1,2 @@ +Cloud = true +RequiresUnityCatalog = true diff --git a/libs/testserver/permissions.go b/libs/testserver/permissions.go index a6610a8b091..61f9907ae2e 100644 --- a/libs/testserver/permissions.go +++ b/libs/testserver/permissions.go @@ -331,7 +331,10 @@ func (s *FakeWorkspace) SetPermissions(req Request) any { if owners != 1 { return Response{ StatusCode: 400, - Body: map[string]string{"message": "The " + ownerNoun + " must have exactly one owner."}, + Body: map[string]string{ + "error_code": "INVALID_PARAMETER_VALUE", + "message": "The " + ownerNoun + " must have exactly one owner.", + }, } } } From 03929691530c93c13f011031d75798186a0b37dd Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Fri, 3 Jul 2026 14:15:44 +0200 Subject: [PATCH 3/6] Fix #5682: run FixPermissions on resources updated by Python mutators Permissions added to an existing resource by a PyDABs job/pipeline mutator went through NormalizeResources, which ran only the normalize mutators and skipped FixPermissions. The deploying user was therefore never added as IS_OWNER, and the direct engine PUT an ownerless ACL that the Permissions API rejects with "must have exactly one owner". The terraform provider re-injected the owner at PUT time, hiding the bug for that engine. Run FixPermissions in NormalizeResources so Python-sourced permissions get the same owner treatment as YAML ones. FixPermissions is idempotent, so re-running it on resources that already have an owner is a no-op; ApplyBundlePermissions is not re-run because it is not idempotent and already ran in ProcessStaticResources. Guard FixPermissions against a nil CurrentUser, which is possible when it runs outside the initialize phase. Co-authored-by: Isaac --- .../mutator-permissions-owner-5682/mutators.py | 9 +++++---- .../out.deploy.direct.txt | 14 -------------- .../out.deploy.terraform.txt | 6 ------ .../out.destroy.direct.txt | 13 ------------- .../out.destroy.terraform.txt | 13 ------------- .../mutator-permissions-owner-5682/output.txt | 9 ++++++++- .../python/mutator-permissions-owner-5682/script | 12 +++++------- .../mutator/resourcemutator/fix_permissions.go | 6 ++++++ .../mutator/resourcemutator/resource_mutator.go | 11 +++++++++++ 9 files changed, 35 insertions(+), 58 deletions(-) delete mode 100644 acceptance/bundle/python/mutator-permissions-owner-5682/out.deploy.direct.txt delete mode 100644 acceptance/bundle/python/mutator-permissions-owner-5682/out.deploy.terraform.txt delete mode 100644 acceptance/bundle/python/mutator-permissions-owner-5682/out.destroy.direct.txt delete mode 100644 acceptance/bundle/python/mutator-permissions-owner-5682/out.destroy.terraform.txt diff --git a/acceptance/bundle/python/mutator-permissions-owner-5682/mutators.py b/acceptance/bundle/python/mutator-permissions-owner-5682/mutators.py index 811a3291034..7d2fdf5a5dc 100644 --- a/acceptance/bundle/python/mutator-permissions-owner-5682/mutators.py +++ b/acceptance/bundle/python/mutator-permissions-owner-5682/mutators.py @@ -3,12 +3,13 @@ from databricks.bundles.core import pipeline_mutator, Bundle -# Reproduces https://github.com/databricks/cli/issues/5682. +# Regression test for https://github.com/databricks/cli/issues/5682. # Adds a permission to a pipeline that is already defined in YAML (mirrors the # reporter's mutator, which reads a bundle variable). Resources updated by a -# PythonMutator go through NormalizeResources, which does NOT run FixPermissions, -# so the deploying user is never added as IS_OWNER. The resulting ownerless -# permissions PUT is rejected with "The pipeline must have exactly one owner". +# PythonMutator go through NormalizeResources; that path now runs FixPermissions, +# so the deploying user is added as IS_OWNER and the permissions PUT succeeds. +# Before the fix the ACL had no owner and the backend rejected the PUT with +# "The pipeline must have exactly one owner". @pipeline_mutator def add_pipeline_permission(bundle: Bundle, pipeline: Pipeline) -> Pipeline: group = bundle.resolve_variable(bundle.variables["grantee_group"]) diff --git a/acceptance/bundle/python/mutator-permissions-owner-5682/out.deploy.direct.txt b/acceptance/bundle/python/mutator-permissions-owner-5682/out.deploy.direct.txt deleted file mode 100644 index bbfa9db81f9..00000000000 --- a/acceptance/bundle/python/mutator-permissions-owner-5682/out.deploy.direct.txt +++ /dev/null @@ -1,14 +0,0 @@ - ->>> errcode uv run [UV_ARGS] -q [CLI] bundle deploy -Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle-[UNIQUE_NAME]/default/files... -Deploying resources... -Error: cannot create resources.pipelines.my_pipeline.permissions: The pipeline must have exactly one owner. (400 INVALID_PARAMETER_VALUE) - -Endpoint: PUT [DATABRICKS_URL]/api/2.0/permissions/pipelines/[UUID] -HTTP Status: 400 Bad Request -API error_code: INVALID_PARAMETER_VALUE -API message: The pipeline must have exactly one owner. - -Updating deployment state... - -Exit code: 1 diff --git a/acceptance/bundle/python/mutator-permissions-owner-5682/out.deploy.terraform.txt b/acceptance/bundle/python/mutator-permissions-owner-5682/out.deploy.terraform.txt deleted file mode 100644 index f78330641b5..00000000000 --- a/acceptance/bundle/python/mutator-permissions-owner-5682/out.deploy.terraform.txt +++ /dev/null @@ -1,6 +0,0 @@ - ->>> errcode uv run [UV_ARGS] -q [CLI] bundle deploy -Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle-[UNIQUE_NAME]/default/files... -Deploying resources... -Updating deployment state... -Deployment complete! diff --git a/acceptance/bundle/python/mutator-permissions-owner-5682/out.destroy.direct.txt b/acceptance/bundle/python/mutator-permissions-owner-5682/out.destroy.direct.txt deleted file mode 100644 index 297e35e6514..00000000000 --- a/acceptance/bundle/python/mutator-permissions-owner-5682/out.destroy.direct.txt +++ /dev/null @@ -1,13 +0,0 @@ - ->>> errcode uv run [UV_ARGS] -q [CLI] bundle destroy --auto-approve -The following resources will be deleted: - delete resources.pipelines.my_pipeline - -This action will result in the deletion of the following Lakeflow Spark Declarative Pipelines along with the -Streaming Tables (STs) and Materialized Views (MVs) managed by them: - delete resources.pipelines.my_pipeline - -All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test-bundle-[UNIQUE_NAME]/default - -Deleting files... -Destroy complete! diff --git a/acceptance/bundle/python/mutator-permissions-owner-5682/out.destroy.terraform.txt b/acceptance/bundle/python/mutator-permissions-owner-5682/out.destroy.terraform.txt deleted file mode 100644 index 297e35e6514..00000000000 --- a/acceptance/bundle/python/mutator-permissions-owner-5682/out.destroy.terraform.txt +++ /dev/null @@ -1,13 +0,0 @@ - ->>> errcode uv run [UV_ARGS] -q [CLI] bundle destroy --auto-approve -The following resources will be deleted: - delete resources.pipelines.my_pipeline - -This action will result in the deletion of the following Lakeflow Spark Declarative Pipelines along with the -Streaming Tables (STs) and Materialized Views (MVs) managed by them: - delete resources.pipelines.my_pipeline - -All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test-bundle-[UNIQUE_NAME]/default - -Deleting files... -Destroy complete! diff --git a/acceptance/bundle/python/mutator-permissions-owner-5682/output.txt b/acceptance/bundle/python/mutator-permissions-owner-5682/output.txt index 24b9a2cb632..20db23925a2 100644 --- a/acceptance/bundle/python/mutator-permissions-owner-5682/output.txt +++ b/acceptance/bundle/python/mutator-permissions-owner-5682/output.txt @@ -1,5 +1,12 @@ >>> uv run [UV_ARGS] -q [CLI] bundle validate --output json [ - "CAN_RUN" + "CAN_RUN", + "IS_OWNER" ] + +>>> uv run [UV_ARGS] -q [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle-[UNIQUE_NAME]/default/files... +Deploying resources... +Updating deployment state... +Deployment complete! diff --git a/acceptance/bundle/python/mutator-permissions-owner-5682/script b/acceptance/bundle/python/mutator-permissions-owner-5682/script index 9e7e6586963..31330842b48 100644 --- a/acceptance/bundle/python/mutator-permissions-owner-5682/script +++ b/acceptance/bundle/python/mutator-permissions-owner-5682/script @@ -8,17 +8,15 @@ if [ -n "$CLOUD_ENV" ]; then fi cleanup() { - trace errcode uv run $UV_ARGS -q $CLI bundle destroy --auto-approve &> out.destroy.$DATABRICKS_BUNDLE_ENGINE.txt + uv run $UV_ARGS -q $CLI bundle destroy --auto-approve &> LOG.destroy rm -fr .databricks __pycache__ } trap cleanup EXIT -# The Python mutator adds a permission but no owner. FixPermissions does not run on -# resources updated by PythonMutator, so no IS_OWNER is added for the current user. +# The Python mutator adds a CAN_RUN permission; FixPermissions then adds IS_OWNER for +# the current user (issue #5682 - previously it did not run on Python-updated resources). trace uv run $UV_ARGS -q $CLI bundle validate --output json | \ jq ".resources.pipelines.my_pipeline.permissions | map(.level)" -# BUG #5682: the direct engine PUTs the ownerless ACL and the backend rejects it with -# "The pipeline must have exactly one owner". The terraform provider re-injects the -# owner at PUT time, so it succeeds. Deploy output therefore differs per engine. -trace errcode uv run $UV_ARGS -q $CLI bundle deploy &> out.deploy.$DATABRICKS_BUNDLE_ENGINE.txt +# With the owner present, the deploy succeeds on both engines. +trace uv run $UV_ARGS -q $CLI bundle deploy diff --git a/bundle/config/mutator/resourcemutator/fix_permissions.go b/bundle/config/mutator/resourcemutator/fix_permissions.go index 7f685015e2d..6d8a44bd800 100644 --- a/bundle/config/mutator/resourcemutator/fix_permissions.go +++ b/bundle/config/mutator/resourcemutator/fix_permissions.go @@ -211,6 +211,12 @@ func createPermissionFromPrincipal(principal, level string) dyn.Value { } func (m *fixPermissions) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics { + // CurrentUser is populated by PopulateCurrentUser early in the initialize phase. + // It can be nil when this mutator runs outside that phase (e.g. NormalizeResources + // after PythonMutator); there is no user to add as owner, so skip. + if b.Config.Workspace.CurrentUser == nil { + return nil + } currentUser := b.Config.Workspace.CurrentUser.UserName err := b.Config.Mutate(func(v dyn.Value) (dyn.Value, error) { diff --git a/bundle/config/mutator/resourcemutator/resource_mutator.go b/bundle/config/mutator/resourcemutator/resource_mutator.go index 4a552a63310..01ea8e8340f 100644 --- a/bundle/config/mutator/resourcemutator/resource_mutator.go +++ b/bundle/config/mutator/resourcemutator/resource_mutator.go @@ -295,6 +295,17 @@ func NormalizeResources( return } + // Permissions added to an existing resource by a Python mutator must still get the + // deploying user as IS_OWNER, otherwise the Permissions API rejects the PUT with + // "must have exactly one owner" (#5682). FixPermissions is idempotent, so re-running + // it on resources that already have an owner is a no-op. ApplyBundlePermissions is + // intentionally not re-run here: it is not idempotent (it appends bundle-level + // permissions) and already ran for these resources in ProcessStaticResources. + bundle.ApplySeqContext(ctx, b, FixPermissions()) + if logdiag.HasError(ctx) { + return + } + // after mutators, we merge updated resources back to snapshot to preserve non-selected resources err = b.Config.Mutate(func(root dyn.Value) (dyn.Value, error) { return mergeResources(root, snapshot) From 803e3e22d818bff9eec32c1f5a79607c7ad0b620 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Fri, 3 Jul 2026 15:54:23 +0200 Subject: [PATCH 4/6] Add changelog entry for #5821 Co-authored-by: Isaac --- NEXT_CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index 57d93cb0fcd..2c07236abf0 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -11,6 +11,7 @@ ### Bundles * `bundle generate job` now downloads workspace files referenced by `spark_python_task`, rewriting them to a relative path like it already does for notebooks. Git-sourced files and cloud URIs are left untouched ([#5799](https://github.com/databricks/cli/pull/5799)). +* Fix permissions added to a job or pipeline by a Python (PyDABs) mutator failing to deploy with "must have exactly one owner"; the deploying identity is now set as owner, matching resources whose permissions are declared in YAML ([#5821](https://github.com/databricks/cli/pull/5821)). ### Dependency updates From 1306a5db5468649d43e7e0f7e1ec190668db7117 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Fri, 3 Jul 2026 16:01:49 +0200 Subject: [PATCH 5/6] dresources: pipeline permissions fixture needs an owner The testserver now enforces exactly-one-owner for pipelines (like jobs), so the pipelines.permissions CRUD fixture must set IS_OWNER instead of CAN_MANAGE. Co-authored-by: Isaac --- bundle/direct/dresources/all_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bundle/direct/dresources/all_test.go b/bundle/direct/dresources/all_test.go index 8994fa328fc..e47ccbe8451 100644 --- a/bundle/direct/dresources/all_test.go +++ b/bundle/direct/dresources/all_test.go @@ -358,7 +358,8 @@ var testDeps = map[string]prepareWorkspace{ return &PermissionsState{ ObjectID: "/pipelines/" + resp.PipelineId, EmbeddedSlice: []StatePermission{{ - Level: "CAN_MANAGE", + // Pipelines require exactly one owner, like jobs. + Level: "IS_OWNER", UserName: "user@example.com", }}, }, nil From cc5aac3a3400a618220b82d68b7b13ff59a86128 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Fri, 3 Jul 2026 16:11:52 +0200 Subject: [PATCH 6/6] Use ApplyContext instead of ApplySeqContext for single mutator Co-authored-by: Isaac --- bundle/config/mutator/resourcemutator/resource_mutator.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundle/config/mutator/resourcemutator/resource_mutator.go b/bundle/config/mutator/resourcemutator/resource_mutator.go index 01ea8e8340f..e8c33f0c59b 100644 --- a/bundle/config/mutator/resourcemutator/resource_mutator.go +++ b/bundle/config/mutator/resourcemutator/resource_mutator.go @@ -301,7 +301,7 @@ func NormalizeResources( // it on resources that already have an owner is a no-op. ApplyBundlePermissions is // intentionally not re-run here: it is not idempotent (it appends bundle-level // permissions) and already ran for these resources in ProcessStaticResources. - bundle.ApplySeqContext(ctx, b, FixPermissions()) + bundle.ApplyContext(ctx, b, FixPermissions()) if logdiag.HasError(ctx) { return }