From e1812fa89da18c074dbee3be38e7aebcb2f8d033 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Fri, 19 Jun 2026 17:22:30 +0200 Subject: [PATCH 1/4] bundle: add DMS auto-migration breakdown telemetry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The merged dms_compat_* verdict tells us whether a deploy is auto-migration compatible, but not why. This adds an independent boolean breakdown recorded on every deploy so the population can be sliced directly: - permissions_section_set: separates the always-compatible no-permissions population from declared-permissions deploys (both land on dms_compat_auto). - state_path_in_deployer_home / state_path_in_other_user_home: where the deployment state lives (with the existing state_path_is_shared; all false means some other /Workspace folder). - dms_undeclared_deploying_user / _other_user / _service_principal / _group: which principal types hold undeclared write access to the state folder — the access an auto-migration governed by the permissions section would drop. These co-occur. All keys go through the existing BoolValues map, so no telemetry proto change is needed. Exact per-type counts would need a numeric proto field and are left out. Covered by the existing deploy-workspace-folder-permissions acceptance test (extended with an other-user-home target) plus unit tests for the principal-type classification and home-owner parsing. Co-authored-by: Shreyas Goenka --- .../job_tasks/out.telemetry.direct.txt | 7 ++ .../job_tasks/out.telemetry.terraform.txt | 7 ++ .../resource_deps/resources_var/output.txt | 7 ++ .../out.telemetry.terraform.txt | 7 ++ .../deploy-app-lifecycle-started/output.txt | 28 +++++++ .../telemetry/deploy-compute-type/output.txt | 56 +++++++++++++ .../telemetry/deploy-experimental/output.txt | 28 +++++++ .../deploy-name-prefix/custom/output.txt | 28 +++++++ .../mode-development/output.txt | 28 +++++++ .../telemetry/deploy-whl-artifacts/output.txt | 56 +++++++++++++ .../databricks.yml | 9 +++ .../output.txt | 53 +++++++++++- .../script | 3 +- .../bundle/telemetry/deploy/out.telemetry.txt | 28 +++++++ bundle/metrics/metrics.go | 22 +++++ bundle/permissions/workspace_root.go | 80 ++++++++++++++++--- bundle/permissions/workspace_root_test.go | 49 ++++++++++++ 17 files changed, 485 insertions(+), 11 deletions(-) diff --git a/acceptance/bundle/resource_deps/job_tasks/out.telemetry.direct.txt b/acceptance/bundle/resource_deps/job_tasks/out.telemetry.direct.txt index 25d32d98382..5f71760b8ee 100644 --- a/acceptance/bundle/resource_deps/job_tasks/out.telemetry.direct.txt +++ b/acceptance/bundle/resource_deps/job_tasks/out.telemetry.direct.txt @@ -1,12 +1,19 @@ dms_compat_auto true +dms_undeclared_deploying_user false +dms_undeclared_group false +dms_undeclared_other_user false +dms_undeclared_service_principal false experimental.use_legacy_run_as false has_classic_interactive_compute false has_classic_job_compute false has_serverless_compute true local.cache.attempt true local.cache.miss true +permissions_section_set false presets_name_prefix_is_set false python_wheel_wrapper_is_set false run_as_set false skip_artifact_cleanup false +state_path_in_deployer_home true +state_path_in_other_user_home false state_path_is_shared false diff --git a/acceptance/bundle/resource_deps/job_tasks/out.telemetry.terraform.txt b/acceptance/bundle/resource_deps/job_tasks/out.telemetry.terraform.txt index 25d32d98382..5f71760b8ee 100644 --- a/acceptance/bundle/resource_deps/job_tasks/out.telemetry.terraform.txt +++ b/acceptance/bundle/resource_deps/job_tasks/out.telemetry.terraform.txt @@ -1,12 +1,19 @@ dms_compat_auto true +dms_undeclared_deploying_user false +dms_undeclared_group false +dms_undeclared_other_user false +dms_undeclared_service_principal false experimental.use_legacy_run_as false has_classic_interactive_compute false has_classic_job_compute false has_serverless_compute true local.cache.attempt true local.cache.miss true +permissions_section_set false presets_name_prefix_is_set false python_wheel_wrapper_is_set false run_as_set false skip_artifact_cleanup false +state_path_in_deployer_home true +state_path_in_other_user_home false state_path_is_shared false diff --git a/acceptance/bundle/resource_deps/resources_var/output.txt b/acceptance/bundle/resource_deps/resources_var/output.txt index eb05259f812..4391d7522e8 100644 --- a/acceptance/bundle/resource_deps/resources_var/output.txt +++ b/acceptance/bundle/resource_deps/resources_var/output.txt @@ -37,14 +37,21 @@ >>> print_telemetry_bool_values dms_compat_auto true +dms_undeclared_deploying_user false +dms_undeclared_group false +dms_undeclared_other_user false +dms_undeclared_service_principal false experimental.use_legacy_run_as false has_classic_interactive_compute false has_classic_job_compute false has_serverless_compute false local.cache.attempt true local.cache.hit true +permissions_section_set false presets_name_prefix_is_set true python_wheel_wrapper_is_set false run_as_set false skip_artifact_cleanup false +state_path_in_deployer_home true +state_path_in_other_user_home false state_path_is_shared false diff --git a/acceptance/bundle/resource_deps/tf_path_only_error/out.telemetry.terraform.txt b/acceptance/bundle/resource_deps/tf_path_only_error/out.telemetry.terraform.txt index 2b07a4f52c0..312e6eb39ca 100644 --- a/acceptance/bundle/resource_deps/tf_path_only_error/out.telemetry.terraform.txt +++ b/acceptance/bundle/resource_deps/tf_path_only_error/out.telemetry.terraform.txt @@ -1,4 +1,8 @@ dms_compat_auto true +dms_undeclared_deploying_user false +dms_undeclared_group false +dms_undeclared_other_user false +dms_undeclared_service_principal false experimental.use_legacy_run_as false has_classic_interactive_compute false has_classic_job_compute false @@ -6,8 +10,11 @@ has_serverless_compute false has_tf_only_references true local.cache.attempt true local.cache.hit true +permissions_section_set false presets_name_prefix_is_set false python_wheel_wrapper_is_set false run_as_set false skip_artifact_cleanup false +state_path_in_deployer_home true +state_path_in_other_user_home false state_path_is_shared false diff --git a/acceptance/bundle/telemetry/deploy-app-lifecycle-started/output.txt b/acceptance/bundle/telemetry/deploy-app-lifecycle-started/output.txt index b829ad36e52..5c64eba59d5 100644 --- a/acceptance/bundle/telemetry/deploy-app-lifecycle-started/output.txt +++ b/acceptance/bundle/telemetry/deploy-app-lifecycle-started/output.txt @@ -41,6 +41,34 @@ Deployment complete! "key": "state_path_is_shared", "value": false }, + { + "key": "permissions_section_set", + "value": false + }, + { + "key": "state_path_in_deployer_home", + "value": true + }, + { + "key": "state_path_in_other_user_home", + "value": false + }, + { + "key": "dms_undeclared_deploying_user", + "value": false + }, + { + "key": "dms_undeclared_other_user", + "value": false + }, + { + "key": "dms_undeclared_service_principal", + "value": false + }, + { + "key": "dms_undeclared_group", + "value": false + }, { "key": "dms_compat_auto", "value": true diff --git a/acceptance/bundle/telemetry/deploy-compute-type/output.txt b/acceptance/bundle/telemetry/deploy-compute-type/output.txt index 7895c1d82e0..ac640f00478 100644 --- a/acceptance/bundle/telemetry/deploy-compute-type/output.txt +++ b/acceptance/bundle/telemetry/deploy-compute-type/output.txt @@ -45,6 +45,34 @@ Deployment complete! "key": "state_path_is_shared", "value": false }, + { + "key": "permissions_section_set", + "value": false + }, + { + "key": "state_path_in_deployer_home", + "value": true + }, + { + "key": "state_path_in_other_user_home", + "value": false + }, + { + "key": "dms_undeclared_deploying_user", + "value": false + }, + { + "key": "dms_undeclared_other_user", + "value": false + }, + { + "key": "dms_undeclared_service_principal", + "value": false + }, + { + "key": "dms_undeclared_group", + "value": false + }, { "key": "dms_compat_auto", "value": true @@ -95,6 +123,34 @@ Deployment complete! "key": "state_path_is_shared", "value": false }, + { + "key": "permissions_section_set", + "value": false + }, + { + "key": "state_path_in_deployer_home", + "value": true + }, + { + "key": "state_path_in_other_user_home", + "value": false + }, + { + "key": "dms_undeclared_deploying_user", + "value": false + }, + { + "key": "dms_undeclared_other_user", + "value": false + }, + { + "key": "dms_undeclared_service_principal", + "value": false + }, + { + "key": "dms_undeclared_group", + "value": false + }, { "key": "dms_compat_auto", "value": true diff --git a/acceptance/bundle/telemetry/deploy-experimental/output.txt b/acceptance/bundle/telemetry/deploy-experimental/output.txt index 665b1fab807..6f7c0a67e72 100644 --- a/acceptance/bundle/telemetry/deploy-experimental/output.txt +++ b/acceptance/bundle/telemetry/deploy-experimental/output.txt @@ -44,6 +44,34 @@ Deployment complete! "key": "state_path_is_shared", "value": false }, + { + "key": "permissions_section_set", + "value": false + }, + { + "key": "state_path_in_deployer_home", + "value": true + }, + { + "key": "state_path_in_other_user_home", + "value": false + }, + { + "key": "dms_undeclared_deploying_user", + "value": false + }, + { + "key": "dms_undeclared_other_user", + "value": false + }, + { + "key": "dms_undeclared_service_principal", + "value": false + }, + { + "key": "dms_undeclared_group", + "value": false + }, { "key": "dms_compat_auto", "value": true diff --git a/acceptance/bundle/telemetry/deploy-name-prefix/custom/output.txt b/acceptance/bundle/telemetry/deploy-name-prefix/custom/output.txt index 8c54c247c38..f83173472c4 100644 --- a/acceptance/bundle/telemetry/deploy-name-prefix/custom/output.txt +++ b/acceptance/bundle/telemetry/deploy-name-prefix/custom/output.txt @@ -40,6 +40,34 @@ Deployment complete! "key": "state_path_is_shared", "value": false }, + { + "key": "permissions_section_set", + "value": false + }, + { + "key": "state_path_in_deployer_home", + "value": true + }, + { + "key": "state_path_in_other_user_home", + "value": false + }, + { + "key": "dms_undeclared_deploying_user", + "value": false + }, + { + "key": "dms_undeclared_other_user", + "value": false + }, + { + "key": "dms_undeclared_service_principal", + "value": false + }, + { + "key": "dms_undeclared_group", + "value": false + }, { "key": "dms_compat_auto", "value": true diff --git a/acceptance/bundle/telemetry/deploy-name-prefix/mode-development/output.txt b/acceptance/bundle/telemetry/deploy-name-prefix/mode-development/output.txt index 9bc57dfd206..60ace1ea42d 100644 --- a/acceptance/bundle/telemetry/deploy-name-prefix/mode-development/output.txt +++ b/acceptance/bundle/telemetry/deploy-name-prefix/mode-development/output.txt @@ -40,6 +40,34 @@ Deployment complete! "key": "state_path_is_shared", "value": false }, + { + "key": "permissions_section_set", + "value": false + }, + { + "key": "state_path_in_deployer_home", + "value": true + }, + { + "key": "state_path_in_other_user_home", + "value": false + }, + { + "key": "dms_undeclared_deploying_user", + "value": false + }, + { + "key": "dms_undeclared_other_user", + "value": false + }, + { + "key": "dms_undeclared_service_principal", + "value": false + }, + { + "key": "dms_undeclared_group", + "value": false + }, { "key": "dms_compat_auto", "value": true diff --git a/acceptance/bundle/telemetry/deploy-whl-artifacts/output.txt b/acceptance/bundle/telemetry/deploy-whl-artifacts/output.txt index a8b577bde3b..45f7e090b4d 100644 --- a/acceptance/bundle/telemetry/deploy-whl-artifacts/output.txt +++ b/acceptance/bundle/telemetry/deploy-whl-artifacts/output.txt @@ -44,6 +44,34 @@ Deployment complete! "key": "state_path_is_shared", "value": false }, + { + "key": "permissions_section_set", + "value": false + }, + { + "key": "state_path_in_deployer_home", + "value": true + }, + { + "key": "state_path_in_other_user_home", + "value": false + }, + { + "key": "dms_undeclared_deploying_user", + "value": false + }, + { + "key": "dms_undeclared_other_user", + "value": false + }, + { + "key": "dms_undeclared_service_principal", + "value": false + }, + { + "key": "dms_undeclared_group", + "value": false + }, { "key": "dms_compat_auto", "value": true @@ -96,6 +124,34 @@ Deployment complete! "key": "state_path_is_shared", "value": false }, + { + "key": "permissions_section_set", + "value": false + }, + { + "key": "state_path_in_deployer_home", + "value": true + }, + { + "key": "state_path_in_other_user_home", + "value": false + }, + { + "key": "dms_undeclared_deploying_user", + "value": false + }, + { + "key": "dms_undeclared_other_user", + "value": false + }, + { + "key": "dms_undeclared_service_principal", + "value": false + }, + { + "key": "dms_undeclared_group", + "value": false + }, { "key": "dms_compat_auto", "value": true diff --git a/acceptance/bundle/telemetry/deploy-workspace-folder-permissions/databricks.yml b/acceptance/bundle/telemetry/deploy-workspace-folder-permissions/databricks.yml index a4a69cbce2d..7b07e22ba9a 100644 --- a/acceptance/bundle/telemetry/deploy-workspace-folder-permissions/databricks.yml +++ b/acceptance/bundle/telemetry/deploy-workspace-folder-permissions/databricks.yml @@ -23,6 +23,15 @@ targets: - group_name: team level: CAN_MANAGE + # The state lives under another user's home, where that user has inherited CAN_MANAGE + # that the bundle does not declare. + other_user_not_declared: + permissions: + - user_name: ${workspace.current_user.userName} + level: CAN_MANAGE + workspace: + state_path: /Workspace/Users/other@example.com/test-bundle-state + # A shared state folder is writable by all workspace users; that access is declared # via group_name: users CAN_MANAGE. shared_users_can_manage: diff --git a/acceptance/bundle/telemetry/deploy-workspace-folder-permissions/output.txt b/acceptance/bundle/telemetry/deploy-workspace-folder-permissions/output.txt index 236a16b41a5..7d1c4ef8dc6 100644 --- a/acceptance/bundle/telemetry/deploy-workspace-folder-permissions/output.txt +++ b/acceptance/bundle/telemetry/deploy-workspace-folder-permissions/output.txt @@ -6,6 +6,13 @@ Deployment complete! >>> print_telemetry_bool_values dms_compat_auto true +dms_undeclared_deploying_user false +dms_undeclared_group false +dms_undeclared_other_user false +dms_undeclared_service_principal false +permissions_section_set false +state_path_in_deployer_home true +state_path_in_other_user_home false state_path_is_shared false >>> [CLI] bundle deploy -t user_declared @@ -15,6 +22,13 @@ Deployment complete! >>> print_telemetry_bool_values dms_compat_auto true +dms_undeclared_deploying_user false +dms_undeclared_group false +dms_undeclared_other_user false +dms_undeclared_service_principal false +permissions_section_set true +state_path_in_deployer_home true +state_path_in_other_user_home false state_path_is_shared false >>> [CLI] bundle deploy -t user_not_declared @@ -36,6 +50,29 @@ Deployment complete! >>> print_telemetry_bool_values dms_compat_only_self_undeclared true +dms_undeclared_deploying_user true +dms_undeclared_group false +dms_undeclared_other_user false +dms_undeclared_service_principal false +permissions_section_set true +state_path_in_deployer_home true +state_path_in_other_user_home false +state_path_is_shared false + +>>> [CLI] bundle deploy -t other_user_not_declared +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle/other_user_not_declared/files... +Deploying resources... +Deployment complete! + +>>> print_telemetry_bool_values +dms_compat_not true +dms_undeclared_deploying_user false +dms_undeclared_group false +dms_undeclared_other_user true +dms_undeclared_service_principal false +permissions_section_set true +state_path_in_deployer_home false +state_path_in_other_user_home true state_path_is_shared false >>> [CLI] bundle deploy -t shared_users_can_manage @@ -49,7 +86,7 @@ Consider using a adding a top-level permissions section such as the following: level: CAN_MANAGE See https://docs.databricks.com/dev-tools/bundles/permissions.html to learn more about permission configuration. - in databricks.yml:30:7 + in databricks.yml:39:7 Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle/shared_users_can_manage/files... Deploying resources... @@ -57,6 +94,13 @@ Deployment complete! >>> print_telemetry_bool_values dms_compat_auto true +dms_undeclared_deploying_user false +dms_undeclared_group false +dms_undeclared_other_user false +dms_undeclared_service_principal false +permissions_section_set true +state_path_in_deployer_home false +state_path_in_other_user_home false state_path_is_shared true >>> [CLI] bundle deploy -t shared_not_declared @@ -66,4 +110,11 @@ Deployment complete! >>> print_telemetry_bool_values dms_compat_not true +dms_undeclared_deploying_user false +dms_undeclared_group true +dms_undeclared_other_user false +dms_undeclared_service_principal false +permissions_section_set true +state_path_in_deployer_home false +state_path_in_other_user_home false state_path_is_shared true diff --git a/acceptance/bundle/telemetry/deploy-workspace-folder-permissions/script b/acceptance/bundle/telemetry/deploy-workspace-folder-permissions/script index 4d11d70c3aa..61a2417e000 100644 --- a/acceptance/bundle/telemetry/deploy-workspace-folder-permissions/script +++ b/acceptance/bundle/telemetry/deploy-workspace-folder-permissions/script @@ -2,9 +2,10 @@ for target in \ no_permissions \ user_declared \ user_not_declared \ + other_user_not_declared \ shared_users_can_manage \ shared_not_declared; do trace $CLI bundle deploy -t "$target" - trace print_telemetry_bool_values | grep -E "state_path|dms_compat" + trace print_telemetry_bool_values | grep -E "state_path|permissions_section|dms_" rm out.requests.txt done diff --git a/acceptance/bundle/telemetry/deploy/out.telemetry.txt b/acceptance/bundle/telemetry/deploy/out.telemetry.txt index 2171a3f680a..1c06f0f3e07 100644 --- a/acceptance/bundle/telemetry/deploy/out.telemetry.txt +++ b/acceptance/bundle/telemetry/deploy/out.telemetry.txt @@ -74,6 +74,34 @@ "key": "state_path_is_shared", "value": false }, + { + "key": "permissions_section_set", + "value": false + }, + { + "key": "state_path_in_deployer_home", + "value": true + }, + { + "key": "state_path_in_other_user_home", + "value": false + }, + { + "key": "dms_undeclared_deploying_user", + "value": false + }, + { + "key": "dms_undeclared_other_user", + "value": false + }, + { + "key": "dms_undeclared_service_principal", + "value": false + }, + { + "key": "dms_undeclared_group", + "value": false + }, { "key": "dms_compat_auto", "value": true diff --git a/bundle/metrics/metrics.go b/bundle/metrics/metrics.go index eadc06cb7dc..c0e60440f4e 100644 --- a/bundle/metrics/metrics.go +++ b/bundle/metrics/metrics.go @@ -39,4 +39,26 @@ const ( DMSCompatAuto = "dms_compat_auto" DMSCompatOnlySelfUndeclared = "dms_compat_only_self_undeclared" DMSCompatNot = "dms_compat_not" + + // Breakdown dimensions recorded on every deploy alongside the verdict above, so the + // DMS auto-migration population can be sliced without inferring it from the verdict. + // Each is an independent boolean. + + // Whether a top-level permissions section is set. The no-permissions case is always + // auto-migration compatible (folder ACLs are mirrored), so this separates the two + // populations that both land on dms_compat_auto. + PermissionsSectionSet = "permissions_section_set" + + // Where the deployment state folder lives. Mutually exclusive with each other and + // with StatePathIsShared; all false means some other /Workspace folder. + StatePathInDeployerHome = "state_path_in_deployer_home" + StatePathInOtherUserHome = "state_path_in_other_user_home" + + // Which principal types have undeclared write access to the state folder — the + // access an auto-migration governed by the permissions section would drop. These can + // co-occur; all false when the deploy is auto-migration compatible. + DMSUndeclaredDeployingUser = "dms_undeclared_deploying_user" + DMSUndeclaredOtherUser = "dms_undeclared_other_user" + DMSUndeclaredServicePrincipal = "dms_undeclared_service_principal" + DMSUndeclaredGroup = "dms_undeclared_group" ) diff --git a/bundle/permissions/workspace_root.go b/bundle/permissions/workspace_root.go index 9bb9065fe80..88ba900b801 100644 --- a/bundle/permissions/workspace_root.go +++ b/bundle/permissions/workspace_root.go @@ -5,6 +5,7 @@ import ( "fmt" "slices" "strconv" + "strings" "github.com/databricks/cli/bundle" "github.com/databricks/cli/bundle/config/resources" @@ -139,16 +140,42 @@ func GetWorkspaceObjectPermissionLevel(bundlePermission string) (workspace.Works // folder's permissions relate to the bundle's declared permissions. stateFolderPerms // is the folder's resulting ACL, or nil when no permissions are declared (no folders // are synced in that case). +// +// Alongside the single auto-migration verdict it emits an independent boolean breakdown +// (state folder location, whether a permissions section is set, and which principal +// types have undeclared write access) so the population can be sliced directly. func recordPermissionMetrics(b *bundle.Bundle, stateFolderPerms *WorkspacePathPermissions) { - b.Metrics.SetBoolValue(metrics.StatePathIsShared, libraries.IsWorkspaceSharedPath(b.Config.Workspace.StatePath)) + statePath := b.Config.Workspace.StatePath + deployer := deployingUserName(b) + + b.Metrics.SetBoolValue(metrics.StatePathIsShared, libraries.IsWorkspaceSharedPath(statePath)) + b.Metrics.SetBoolValue(metrics.PermissionsSectionSet, len(b.Config.Permissions) > 0) + + owner, underUserHome := userHomeOwner(statePath) + b.Metrics.SetBoolValue(metrics.StatePathInDeployerHome, underUserHome && deployer != "" && owner == deployer) + b.Metrics.SetBoolValue(metrics.StatePathInOtherUserHome, underUserHome && owner != deployer) + + // stateFolderPerms is nil when no permissions are declared, in which case there are + // no undeclared writers (the migration mirrors the folder's ACLs). + var undeclared []resources.Permission + if stateFolderPerms != nil { + undeclared = stateFolderPerms.UndeclaredWriters(b.Config.Permissions) + } + self, otherUser, servicePrincipal, group := undeclaredWriterTypes(undeclared, deployer) + b.Metrics.SetBoolValue(metrics.DMSUndeclaredDeployingUser, self) + b.Metrics.SetBoolValue(metrics.DMSUndeclaredOtherUser, otherUser) + b.Metrics.SetBoolValue(metrics.DMSUndeclaredServicePrincipal, servicePrincipal) + b.Metrics.SetBoolValue(metrics.DMSUndeclaredGroup, group) + // Emit exactly one of the auto-migration verdict keys. - b.Metrics.SetBoolValue(autoMigrationVerdict(b, stateFolderPerms), true) + b.Metrics.SetBoolValue(autoMigrationVerdict(b, stateFolderPerms, undeclared), true) } // autoMigrationVerdict returns the metric key describing whether this deploy is // compatible with an automatic migration of the deployment state to a dedicated -// state storage service. See metrics.DMSCompatAuto. -func autoMigrationVerdict(b *bundle.Bundle, stateFolderPerms *WorkspacePathPermissions) string { +// state storage service. undeclared is the state folder's undeclared writers (empty +// when no permissions are declared). See metrics.DMSCompatAuto. +func autoMigrationVerdict(b *bundle.Bundle, stateFolderPerms *WorkspacePathPermissions, undeclared []resources.Permission) string { // No permissions section: the migration mirrors the state folder's ACLs onto the // deployment (CAN_EDIT -> CAN_EDIT, CAN_MANAGE -> CAN_MANAGE), preserving // everyone's access wherever the state lives. @@ -167,7 +194,6 @@ func autoMigrationVerdict(b *bundle.Bundle, stateFolderPerms *WorkspacePathPermi // The migration applies exactly the declared permissions to the deployment, so // anyone with write access to the state folder who is not declared loses the // ability to deploy. - undeclared := stateFolderPerms.UndeclaredWriters(b.Config.Permissions) switch { case len(undeclared) == 0: return metrics.DMSCompatAuto @@ -181,10 +207,46 @@ func autoMigrationVerdict(b *bundle.Bundle, stateFolderPerms *WorkspacePathPermi } } -// isDeployingUser reports whether p is the user performing the deploy. -func isDeployingUser(b *bundle.Bundle, p resources.Permission) bool { +// undeclaredWriterTypes classifies undeclared writers by principal type, distinguishing +// the deploying user from other users. +func undeclaredWriterTypes(undeclared []resources.Permission, deployer string) (self, otherUser, servicePrincipal, group bool) { + for _, p := range undeclared { + switch { + case p.UserName != "" && p.UserName == deployer: + self = true + case p.UserName != "": + otherUser = true + case p.ServicePrincipalName != "": + servicePrincipal = true + case p.GroupName != "": + group = true + } + } + return self, otherUser, servicePrincipal, group +} + +// userHomeOwner returns the owner of the user home folder containing path, i.e. +// for a path under /Workspace/Users/. ok is false when path is not under a user +// home folder. +func userHomeOwner(path string) (owner string, ok bool) { + const prefix = "/Workspace/Users/" + if !strings.HasPrefix(path, prefix) { + return "", false + } + owner, _, _ = strings.Cut(path[len(prefix):], "/") + return owner, owner != "" +} + +// deployingUserName returns the user performing the deploy, or "" when not yet resolved. +func deployingUserName(b *bundle.Bundle) string { if b.Config.Workspace.CurrentUser == nil { - return false + return "" } - return p.UserName != "" && p.UserName == b.Config.Workspace.CurrentUser.UserName + return b.Config.Workspace.CurrentUser.UserName +} + +// isDeployingUser reports whether p is the user performing the deploy. +func isDeployingUser(b *bundle.Bundle, p resources.Permission) bool { + deployer := deployingUserName(b) + return p.UserName != "" && p.UserName == deployer } diff --git a/bundle/permissions/workspace_root_test.go b/bundle/permissions/workspace_root_test.go index c97de014bba..db4ea36fc8c 100644 --- a/bundle/permissions/workspace_root_test.go +++ b/bundle/permissions/workspace_root_test.go @@ -188,3 +188,52 @@ func TestApplyWorkspaceRootPermissionsForAllPaths(t *testing.T) { diags := bundle.Apply(t.Context(), b, ApplyWorkspaceRootPermissions()) require.NoError(t, diags.Error()) } + +func TestUndeclaredWriterTypes(t *testing.T) { + const deployer = "me@example.com" + self := resources.Permission{Level: CAN_MANAGE, UserName: deployer} + other := resources.Permission{Level: CAN_MANAGE, UserName: "other@example.com"} + sp := resources.Permission{Level: CAN_MANAGE, ServicePrincipalName: "sp-1"} + group := resources.Permission{Level: CAN_MANAGE, GroupName: "team"} + + cases := []struct { + name string + undeclared []resources.Permission + wantSelf, wantOther, wantSP, wantGroup bool + }{ + {"empty", nil, false, false, false, false}, + {"deploying user", []resources.Permission{self}, true, false, false, false}, + {"other user", []resources.Permission{other}, false, true, false, false}, + {"service principal", []resources.Permission{sp}, false, false, true, false}, + {"group", []resources.Permission{group}, false, false, false, true}, + {"all types", []resources.Permission{self, other, sp, group}, true, true, true, true}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + gotSelf, gotOther, gotSP, gotGroup := undeclaredWriterTypes(tc.undeclared, deployer) + require.Equal(t, tc.wantSelf, gotSelf) + require.Equal(t, tc.wantOther, gotOther) + require.Equal(t, tc.wantSP, gotSP) + require.Equal(t, tc.wantGroup, gotGroup) + }) + } +} + +func TestUserHomeOwner(t *testing.T) { + cases := []struct { + path string + owner string + ok bool + }{ + {"/Workspace/Users/alice@example.com/.bundle/x/state", "alice@example.com", true}, + {"/Workspace/Users/alice@example.com", "alice@example.com", true}, + {"/Workspace/Shared/state", "", false}, + {"/Workspace/team/state", "", false}, + {"/Workspace/Users/", "", false}, + } + for _, tc := range cases { + owner, ok := userHomeOwner(tc.path) + require.Equal(t, tc.ok, ok, tc.path) + require.Equal(t, tc.owner, owner, tc.path) + } +} From 0737461f2d34d472ed34528594f410b942793626 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Mon, 22 Jun 2026 10:07:07 +0200 Subject: [PATCH 2/4] bundle: simplify state-folder home-location classification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The deployer != "" guard on state_path_in_deployer_home was redundant — userHomeOwner returns a non-empty owner whenever the path is under a user home, so an unresolved deployer can never match. Drop it so the deployer/other-user home flags are exact complements, and document the invariant. Co-authored-by: Shreyas Goenka --- bundle/permissions/workspace_root.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bundle/permissions/workspace_root.go b/bundle/permissions/workspace_root.go index 88ba900b801..f4033a890cc 100644 --- a/bundle/permissions/workspace_root.go +++ b/bundle/permissions/workspace_root.go @@ -151,8 +151,11 @@ func recordPermissionMetrics(b *bundle.Bundle, stateFolderPerms *WorkspacePathPe b.Metrics.SetBoolValue(metrics.StatePathIsShared, libraries.IsWorkspaceSharedPath(statePath)) b.Metrics.SetBoolValue(metrics.PermissionsSectionSet, len(b.Config.Permissions) > 0) + // userHomeOwner yields a non-empty owner whenever underUserHome is true, so these + // are exact complements: an unresolved deployer ("") never equals the owner and + // falls into the other-user bucket. owner, underUserHome := userHomeOwner(statePath) - b.Metrics.SetBoolValue(metrics.StatePathInDeployerHome, underUserHome && deployer != "" && owner == deployer) + b.Metrics.SetBoolValue(metrics.StatePathInDeployerHome, underUserHome && owner == deployer) b.Metrics.SetBoolValue(metrics.StatePathInOtherUserHome, underUserHome && owner != deployer) // stateFolderPerms is nil when no permissions are declared, in which case there are From 526e2ff7e3b0487867fc9994ed76fc319e08dc86 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Mon, 22 Jun 2026 10:18:51 +0200 Subject: [PATCH 3/4] bundle: document /Workspace prefix invariant in userHomeOwner Co-authored-by: Shreyas Goenka --- bundle/permissions/workspace_root.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bundle/permissions/workspace_root.go b/bundle/permissions/workspace_root.go index f4033a890cc..73950cf0df3 100644 --- a/bundle/permissions/workspace_root.go +++ b/bundle/permissions/workspace_root.go @@ -231,6 +231,10 @@ func undeclaredWriterTypes(undeclared []resources.Permission, deployer string) ( // userHomeOwner returns the owner of the user home folder containing path, i.e. // for a path under /Workspace/Users/. ok is false when path is not under a user // home folder. +// +// Paths are always /Workspace-prefixed here: PrependWorkspacePrefix runs in the +// initialize phase (before this deploy-phase mutator) and rewrites a bare /Users/... +// to /Workspace/Users/..., so the /Workspace/Users/ check below is sufficient. func userHomeOwner(path string) (owner string, ok bool) { const prefix = "/Workspace/Users/" if !strings.HasPrefix(path, prefix) { From 3e98421b3fae41dccae0081311e2eda945432e16 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Mon, 22 Jun 2026 11:27:46 +0200 Subject: [PATCH 4/4] bundle: add state_path_other location breakdown key Completes the state-folder location breakdown with an explicit bucket for any /Workspace folder that is neither a user home nor shared, so exactly one of the four location keys is true per deploy without deriving "other" by elimination. Co-authored-by: Shreyas Goenka --- .../job_tasks/out.telemetry.direct.txt | 1 + .../job_tasks/out.telemetry.terraform.txt | 1 + .../resource_deps/resources_var/output.txt | 1 + .../out.telemetry.terraform.txt | 1 + .../deploy-app-lifecycle-started/output.txt | 4 +++ .../telemetry/deploy-compute-type/output.txt | 8 ++++++ .../telemetry/deploy-experimental/output.txt | 4 +++ .../deploy-name-prefix/custom/output.txt | 4 +++ .../mode-development/output.txt | 4 +++ .../telemetry/deploy-whl-artifacts/output.txt | 8 ++++++ .../databricks.yml | 8 ++++++ .../output.txt | 25 ++++++++++++++++++- .../script | 1 + .../bundle/telemetry/deploy/out.telemetry.txt | 4 +++ bundle/metrics/metrics.go | 6 +++-- bundle/permissions/workspace_root.go | 14 +++++++---- 16 files changed, 86 insertions(+), 8 deletions(-) diff --git a/acceptance/bundle/resource_deps/job_tasks/out.telemetry.direct.txt b/acceptance/bundle/resource_deps/job_tasks/out.telemetry.direct.txt index 5f71760b8ee..bbd857524ea 100644 --- a/acceptance/bundle/resource_deps/job_tasks/out.telemetry.direct.txt +++ b/acceptance/bundle/resource_deps/job_tasks/out.telemetry.direct.txt @@ -17,3 +17,4 @@ skip_artifact_cleanup false state_path_in_deployer_home true state_path_in_other_user_home false state_path_is_shared false +state_path_other false diff --git a/acceptance/bundle/resource_deps/job_tasks/out.telemetry.terraform.txt b/acceptance/bundle/resource_deps/job_tasks/out.telemetry.terraform.txt index 5f71760b8ee..bbd857524ea 100644 --- a/acceptance/bundle/resource_deps/job_tasks/out.telemetry.terraform.txt +++ b/acceptance/bundle/resource_deps/job_tasks/out.telemetry.terraform.txt @@ -17,3 +17,4 @@ skip_artifact_cleanup false state_path_in_deployer_home true state_path_in_other_user_home false state_path_is_shared false +state_path_other false diff --git a/acceptance/bundle/resource_deps/resources_var/output.txt b/acceptance/bundle/resource_deps/resources_var/output.txt index 4391d7522e8..34e6a69b57a 100644 --- a/acceptance/bundle/resource_deps/resources_var/output.txt +++ b/acceptance/bundle/resource_deps/resources_var/output.txt @@ -55,3 +55,4 @@ skip_artifact_cleanup false state_path_in_deployer_home true state_path_in_other_user_home false state_path_is_shared false +state_path_other false diff --git a/acceptance/bundle/resource_deps/tf_path_only_error/out.telemetry.terraform.txt b/acceptance/bundle/resource_deps/tf_path_only_error/out.telemetry.terraform.txt index 312e6eb39ca..9249391ce43 100644 --- a/acceptance/bundle/resource_deps/tf_path_only_error/out.telemetry.terraform.txt +++ b/acceptance/bundle/resource_deps/tf_path_only_error/out.telemetry.terraform.txt @@ -18,3 +18,4 @@ skip_artifact_cleanup false state_path_in_deployer_home true state_path_in_other_user_home false state_path_is_shared false +state_path_other false diff --git a/acceptance/bundle/telemetry/deploy-app-lifecycle-started/output.txt b/acceptance/bundle/telemetry/deploy-app-lifecycle-started/output.txt index 5c64eba59d5..3663f140db9 100644 --- a/acceptance/bundle/telemetry/deploy-app-lifecycle-started/output.txt +++ b/acceptance/bundle/telemetry/deploy-app-lifecycle-started/output.txt @@ -53,6 +53,10 @@ Deployment complete! "key": "state_path_in_other_user_home", "value": false }, + { + "key": "state_path_other", + "value": false + }, { "key": "dms_undeclared_deploying_user", "value": false diff --git a/acceptance/bundle/telemetry/deploy-compute-type/output.txt b/acceptance/bundle/telemetry/deploy-compute-type/output.txt index ac640f00478..9c59aba8e71 100644 --- a/acceptance/bundle/telemetry/deploy-compute-type/output.txt +++ b/acceptance/bundle/telemetry/deploy-compute-type/output.txt @@ -57,6 +57,10 @@ Deployment complete! "key": "state_path_in_other_user_home", "value": false }, + { + "key": "state_path_other", + "value": false + }, { "key": "dms_undeclared_deploying_user", "value": false @@ -135,6 +139,10 @@ Deployment complete! "key": "state_path_in_other_user_home", "value": false }, + { + "key": "state_path_other", + "value": false + }, { "key": "dms_undeclared_deploying_user", "value": false diff --git a/acceptance/bundle/telemetry/deploy-experimental/output.txt b/acceptance/bundle/telemetry/deploy-experimental/output.txt index 6f7c0a67e72..df24e19a74d 100644 --- a/acceptance/bundle/telemetry/deploy-experimental/output.txt +++ b/acceptance/bundle/telemetry/deploy-experimental/output.txt @@ -56,6 +56,10 @@ Deployment complete! "key": "state_path_in_other_user_home", "value": false }, + { + "key": "state_path_other", + "value": false + }, { "key": "dms_undeclared_deploying_user", "value": false diff --git a/acceptance/bundle/telemetry/deploy-name-prefix/custom/output.txt b/acceptance/bundle/telemetry/deploy-name-prefix/custom/output.txt index f83173472c4..f68b3f24975 100644 --- a/acceptance/bundle/telemetry/deploy-name-prefix/custom/output.txt +++ b/acceptance/bundle/telemetry/deploy-name-prefix/custom/output.txt @@ -52,6 +52,10 @@ Deployment complete! "key": "state_path_in_other_user_home", "value": false }, + { + "key": "state_path_other", + "value": false + }, { "key": "dms_undeclared_deploying_user", "value": false diff --git a/acceptance/bundle/telemetry/deploy-name-prefix/mode-development/output.txt b/acceptance/bundle/telemetry/deploy-name-prefix/mode-development/output.txt index 60ace1ea42d..da51756db48 100644 --- a/acceptance/bundle/telemetry/deploy-name-prefix/mode-development/output.txt +++ b/acceptance/bundle/telemetry/deploy-name-prefix/mode-development/output.txt @@ -52,6 +52,10 @@ Deployment complete! "key": "state_path_in_other_user_home", "value": false }, + { + "key": "state_path_other", + "value": false + }, { "key": "dms_undeclared_deploying_user", "value": false diff --git a/acceptance/bundle/telemetry/deploy-whl-artifacts/output.txt b/acceptance/bundle/telemetry/deploy-whl-artifacts/output.txt index 45f7e090b4d..7b8678cf5b7 100644 --- a/acceptance/bundle/telemetry/deploy-whl-artifacts/output.txt +++ b/acceptance/bundle/telemetry/deploy-whl-artifacts/output.txt @@ -56,6 +56,10 @@ Deployment complete! "key": "state_path_in_other_user_home", "value": false }, + { + "key": "state_path_other", + "value": false + }, { "key": "dms_undeclared_deploying_user", "value": false @@ -136,6 +140,10 @@ Deployment complete! "key": "state_path_in_other_user_home", "value": false }, + { + "key": "state_path_other", + "value": false + }, { "key": "dms_undeclared_deploying_user", "value": false diff --git a/acceptance/bundle/telemetry/deploy-workspace-folder-permissions/databricks.yml b/acceptance/bundle/telemetry/deploy-workspace-folder-permissions/databricks.yml index 7b07e22ba9a..ac728ed8a0f 100644 --- a/acceptance/bundle/telemetry/deploy-workspace-folder-permissions/databricks.yml +++ b/acceptance/bundle/telemetry/deploy-workspace-folder-permissions/databricks.yml @@ -32,6 +32,14 @@ targets: workspace: state_path: /Workspace/Users/other@example.com/test-bundle-state + # The state lives in a non-home, non-shared workspace folder. + workspace_other: + permissions: + - user_name: ${workspace.current_user.userName} + level: CAN_MANAGE + workspace: + state_path: /Workspace/teams/test-bundle-state + # A shared state folder is writable by all workspace users; that access is declared # via group_name: users CAN_MANAGE. shared_users_can_manage: diff --git a/acceptance/bundle/telemetry/deploy-workspace-folder-permissions/output.txt b/acceptance/bundle/telemetry/deploy-workspace-folder-permissions/output.txt index 7d1c4ef8dc6..faeda1df1fd 100644 --- a/acceptance/bundle/telemetry/deploy-workspace-folder-permissions/output.txt +++ b/acceptance/bundle/telemetry/deploy-workspace-folder-permissions/output.txt @@ -14,6 +14,7 @@ permissions_section_set false state_path_in_deployer_home true state_path_in_other_user_home false state_path_is_shared false +state_path_other false >>> [CLI] bundle deploy -t user_declared Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle/user_declared/files... @@ -30,6 +31,7 @@ permissions_section_set true state_path_in_deployer_home true state_path_in_other_user_home false state_path_is_shared false +state_path_other false >>> [CLI] bundle deploy -t user_not_declared Recommendation: permissions section should explicitly include the current deployment identity '[USERNAME]' or one of its groups @@ -58,6 +60,7 @@ permissions_section_set true state_path_in_deployer_home true state_path_in_other_user_home false state_path_is_shared false +state_path_other false >>> [CLI] bundle deploy -t other_user_not_declared Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle/other_user_not_declared/files... @@ -74,6 +77,24 @@ permissions_section_set true state_path_in_deployer_home false state_path_in_other_user_home true state_path_is_shared false +state_path_other false + +>>> [CLI] bundle deploy -t workspace_other +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle/workspace_other/files... +Deploying resources... +Deployment complete! + +>>> print_telemetry_bool_values +dms_compat_auto true +dms_undeclared_deploying_user false +dms_undeclared_group false +dms_undeclared_other_user false +dms_undeclared_service_principal false +permissions_section_set true +state_path_in_deployer_home false +state_path_in_other_user_home false +state_path_is_shared false +state_path_other true >>> [CLI] bundle deploy -t shared_users_can_manage Recommendation: permissions section should explicitly include the current deployment identity '[USERNAME]' or one of its groups @@ -86,7 +107,7 @@ Consider using a adding a top-level permissions section such as the following: level: CAN_MANAGE See https://docs.databricks.com/dev-tools/bundles/permissions.html to learn more about permission configuration. - in databricks.yml:39:7 + in databricks.yml:47:7 Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle/shared_users_can_manage/files... Deploying resources... @@ -102,6 +123,7 @@ permissions_section_set true state_path_in_deployer_home false state_path_in_other_user_home false state_path_is_shared true +state_path_other false >>> [CLI] bundle deploy -t shared_not_declared Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle/shared_not_declared/files... @@ -118,3 +140,4 @@ permissions_section_set true state_path_in_deployer_home false state_path_in_other_user_home false state_path_is_shared true +state_path_other false diff --git a/acceptance/bundle/telemetry/deploy-workspace-folder-permissions/script b/acceptance/bundle/telemetry/deploy-workspace-folder-permissions/script index 61a2417e000..e924cd0e2b0 100644 --- a/acceptance/bundle/telemetry/deploy-workspace-folder-permissions/script +++ b/acceptance/bundle/telemetry/deploy-workspace-folder-permissions/script @@ -3,6 +3,7 @@ for target in \ user_declared \ user_not_declared \ other_user_not_declared \ + workspace_other \ shared_users_can_manage \ shared_not_declared; do trace $CLI bundle deploy -t "$target" diff --git a/acceptance/bundle/telemetry/deploy/out.telemetry.txt b/acceptance/bundle/telemetry/deploy/out.telemetry.txt index 1c06f0f3e07..1a4b5cbb143 100644 --- a/acceptance/bundle/telemetry/deploy/out.telemetry.txt +++ b/acceptance/bundle/telemetry/deploy/out.telemetry.txt @@ -86,6 +86,10 @@ "key": "state_path_in_other_user_home", "value": false }, + { + "key": "state_path_other", + "value": false + }, { "key": "dms_undeclared_deploying_user", "value": false diff --git a/bundle/metrics/metrics.go b/bundle/metrics/metrics.go index c0e60440f4e..f5238e85202 100644 --- a/bundle/metrics/metrics.go +++ b/bundle/metrics/metrics.go @@ -49,10 +49,12 @@ const ( // populations that both land on dms_compat_auto. PermissionsSectionSet = "permissions_section_set" - // Where the deployment state folder lives. Mutually exclusive with each other and - // with StatePathIsShared; all false means some other /Workspace folder. + // Where the deployment state folder lives. Exactly one of StatePathIsShared, + // StatePathInDeployerHome, StatePathInOtherUserHome, and StatePathOther is true per + // deploy. StatePathOther is any other /Workspace folder (not a user home or shared). StatePathInDeployerHome = "state_path_in_deployer_home" StatePathInOtherUserHome = "state_path_in_other_user_home" + StatePathOther = "state_path_other" // Which principal types have undeclared write access to the state folder — the // access an auto-migration governed by the permissions section would drop. These can diff --git a/bundle/permissions/workspace_root.go b/bundle/permissions/workspace_root.go index 73950cf0df3..91e21e69a81 100644 --- a/bundle/permissions/workspace_root.go +++ b/bundle/permissions/workspace_root.go @@ -148,15 +148,19 @@ func recordPermissionMetrics(b *bundle.Bundle, stateFolderPerms *WorkspacePathPe statePath := b.Config.Workspace.StatePath deployer := deployingUserName(b) - b.Metrics.SetBoolValue(metrics.StatePathIsShared, libraries.IsWorkspaceSharedPath(statePath)) + isShared := libraries.IsWorkspaceSharedPath(statePath) + owner, underUserHome := userHomeOwner(statePath) + + b.Metrics.SetBoolValue(metrics.StatePathIsShared, isShared) b.Metrics.SetBoolValue(metrics.PermissionsSectionSet, len(b.Config.Permissions) > 0) - // userHomeOwner yields a non-empty owner whenever underUserHome is true, so these - // are exact complements: an unresolved deployer ("") never equals the owner and - // falls into the other-user bucket. - owner, underUserHome := userHomeOwner(statePath) + // userHomeOwner yields a non-empty owner whenever underUserHome is true, so the two + // home flags are exact complements: an unresolved deployer ("") never equals the + // owner and falls into the other-user bucket. StatePathOther covers any /Workspace + // folder that is neither shared nor a user home. b.Metrics.SetBoolValue(metrics.StatePathInDeployerHome, underUserHome && owner == deployer) b.Metrics.SetBoolValue(metrics.StatePathInOtherUserHome, underUserHome && owner != deployer) + b.Metrics.SetBoolValue(metrics.StatePathOther, !isShared && !underUserHome) // stateFolderPerms is nil when no permissions are declared, in which case there are // no undeclared writers (the migration mirrors the folder's ACLs).