Skip to content

Commit c2aa303

Browse files
bundle: warn when a workspace path is in /Workspace/Shared without users CAN_MANAGE
Renames ValidateSharedRootPermissions to ValidateWorkspaceSharedPermissions and extends it to also cover workspace.state_path. It warns when root_path or state_path is in /Workspace/Shared — granting read/write to all workspace users — but the top-level permissions section does not declare that access via group_name: users CAN_MANAGE. The state_path warning is suppressed only when state_path is nested under root_path, since the root warning already covers it. When state_path is a separate shared folder, both warnings fire. Co-authored-by: Shreyas Goenka <shreyas.goenka@databricks.com>
1 parent e92f999 commit c2aa303

4 files changed

Lines changed: 157 additions & 53 deletions

File tree

bundle/permissions/validate.go

Lines changed: 53 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,49 +3,78 @@ package permissions
33
import (
44
"context"
55
"fmt"
6+
"strings"
67

78
"github.com/databricks/cli/bundle"
89
"github.com/databricks/cli/bundle/libraries"
910
"github.com/databricks/cli/libs/diag"
1011
)
1112

12-
type validateSharedRootPermissions struct{}
13+
type validateWorkspaceSharedPermissions struct{}
1314

14-
func ValidateSharedRootPermissions() bundle.Mutator {
15-
return &validateSharedRootPermissions{}
15+
// ValidateWorkspaceSharedPermissions warns when a workspace path is configured
16+
// under /Workspace/Shared — which grants read/write access to all workspace users —
17+
// without the top-level permissions section declaring that broad access via
18+
// group_name: users with CAN_MANAGE.
19+
func ValidateWorkspaceSharedPermissions() bundle.Mutator {
20+
return &validateWorkspaceSharedPermissions{}
1621
}
1722

18-
func (*validateSharedRootPermissions) Name() string {
19-
return "ValidateSharedRootPermissions"
23+
func (*validateWorkspaceSharedPermissions) Name() string {
24+
return "ValidateWorkspaceSharedPermissions"
2025
}
2126

22-
func (*validateSharedRootPermissions) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
23-
if libraries.IsWorkspaceSharedPath(b.Config.Workspace.RootPath) {
24-
return isUsersGroupPermissionSet(b)
25-
}
26-
27-
return nil
28-
}
29-
30-
// isUsersGroupPermissionSet checks that top-level permissions set for bundle contain group_name: users with CAN_MANAGE permission.
31-
func isUsersGroupPermissionSet(b *bundle.Bundle) diag.Diagnostics {
27+
func (*validateWorkspaceSharedPermissions) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
3228
var diags diag.Diagnostics
3329

34-
allUsers := false
35-
for _, p := range b.Config.Permissions {
36-
if p.GroupName == "users" && p.Level == CAN_MANAGE {
37-
allUsers = true
38-
break
39-
}
30+
rootPath := b.Config.Workspace.RootPath
31+
statePath := b.Config.Workspace.StatePath
32+
rootIsShared := libraries.IsWorkspaceSharedPath(rootPath)
33+
usersCanManage := hasUsersGroupManagePermission(b)
34+
35+
// root_path is in /Workspace/Shared without users CAN_MANAGE.
36+
if rootIsShared && !usersCanManage {
37+
diags = diags.Append(diag.Diagnostic{
38+
Severity: diag.Warning,
39+
Summary: fmt.Sprintf("the bundle root path %s is writable by all workspace users", rootPath),
40+
Detail: "The bundle root path is in /Workspace/Shared, giving read/write access to all workspace users that is not reflected in the permissions section. If this is intentional, add CAN_MANAGE for 'group_name: users' to your bundle permissions. Otherwise, move the bundle to a restricted path such as /Workspace/Users/<username>.",
41+
})
4042
}
4143

42-
if !allUsers {
44+
// state_path is in /Workspace/Shared without users CAN_MANAGE. Skip only when
45+
// state_path is nested under root_path, since the root warning above already
46+
// covers it. When state_path is a separate folder, warn about it on its own.
47+
if libraries.IsWorkspaceSharedPath(statePath) && !statePathUnderRootPath(rootPath, statePath) && !usersCanManage {
4348
diags = diags.Append(diag.Diagnostic{
4449
Severity: diag.Warning,
45-
Summary: fmt.Sprintf("the bundle root path %s is writable by all workspace users", b.Config.Workspace.RootPath),
46-
Detail: "The bundle is configured to use /Workspace/Shared, which will give read/write access to all users. If this is intentional, add CAN_MANAGE for 'group_name: users' permission to your bundle configuration. If the deployment should be restricted, move it to a restricted folder such as /Workspace/Users/<username or principal name>.",
50+
Summary: fmt.Sprintf("the bundle state path %s is writable by all workspace users", statePath),
51+
Detail: "The bundle state path is in /Workspace/Shared, giving read/write access to all workspace users that is not reflected in the permissions section. If this is intentional, add CAN_MANAGE for 'group_name: users' to your bundle permissions. Otherwise, move the state path to a restricted location such as /Workspace/Users/<username>.",
4752
})
4853
}
4954

5055
return diags
5156
}
57+
58+
// hasUsersGroupManagePermission returns true if the top-level permissions include
59+
// group_name: users with CAN_MANAGE level.
60+
func hasUsersGroupManagePermission(b *bundle.Bundle) bool {
61+
for _, p := range b.Config.Permissions {
62+
if p.GroupName == "users" && p.Level == CAN_MANAGE {
63+
return true
64+
}
65+
}
66+
return false
67+
}
68+
69+
// statePathUnderRootPath returns true when statePath is nested under rootPath, in
70+
// which case permissions applied to root_path also cover the state directory.
71+
func statePathUnderRootPath(rootPath, statePath string) bool {
72+
if rootPath == "" || statePath == "" {
73+
return false
74+
}
75+
prefix := rootPath
76+
if !strings.HasSuffix(prefix, "/") {
77+
prefix += "/"
78+
}
79+
return strings.HasPrefix(statePath, prefix)
80+
}

bundle/permissions/validate_test.go

Lines changed: 99 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,18 @@ import (
88
"github.com/databricks/cli/bundle/config/resources"
99
"github.com/databricks/cli/libs/diag"
1010
"github.com/databricks/databricks-sdk-go/experimental/mocks"
11-
"github.com/databricks/databricks-sdk-go/service/jobs"
11+
"github.com/stretchr/testify/assert"
1212
"github.com/stretchr/testify/require"
1313
)
1414

15-
func TestValidateSharedRootPermissionsForShared(t *testing.T) {
15+
func applyValidate(t *testing.T, b *bundle.Bundle) diag.Diagnostics {
16+
t.Helper()
17+
m := mocks.NewMockWorkspaceClient(t)
18+
b.SetWorkpaceClient(m.WorkspaceClient)
19+
return bundle.Apply(t.Context(), b, ValidateWorkspaceSharedPermissions())
20+
}
21+
22+
func TestValidateWorkspaceSharedPermissions_RootShared_NoWarningWithUsersManage(t *testing.T) {
1623
b := &bundle.Bundle{
1724
Config: config.Root{
1825
Workspace: config.Workspace{
@@ -21,23 +28,13 @@ func TestValidateSharedRootPermissionsForShared(t *testing.T) {
2128
Permissions: []resources.Permission{
2229
{Level: CAN_MANAGE, GroupName: "users"},
2330
},
24-
Resources: config.Resources{
25-
Jobs: map[string]*resources.Job{
26-
"job_1": {JobSettings: jobs.JobSettings{Name: "job_1"}},
27-
"job_2": {JobSettings: jobs.JobSettings{Name: "job_2"}},
28-
},
29-
},
3031
},
3132
}
32-
33-
m := mocks.NewMockWorkspaceClient(t)
34-
b.SetWorkpaceClient(m.WorkspaceClient)
35-
36-
diags := bundle.Apply(t.Context(), b, ValidateSharedRootPermissions())
33+
diags := applyValidate(t, b)
3734
require.Empty(t, diags)
3835
}
3936

40-
func TestValidateSharedRootPermissionsForSharedError(t *testing.T) {
37+
func TestValidateWorkspaceSharedPermissions_RootShared_WarnWithoutUsersManage(t *testing.T) {
4138
b := &bundle.Bundle{
4239
Config: config.Root{
4340
Workspace: config.Workspace{
@@ -46,20 +43,98 @@ func TestValidateSharedRootPermissionsForSharedError(t *testing.T) {
4643
Permissions: []resources.Permission{
4744
{Level: CAN_MANAGE, UserName: "foo@bar.test"},
4845
},
49-
Resources: config.Resources{
50-
Jobs: map[string]*resources.Job{
51-
"job_1": {JobSettings: jobs.JobSettings{Name: "job_1"}},
52-
"job_2": {JobSettings: jobs.JobSettings{Name: "job_2"}},
53-
},
46+
},
47+
}
48+
diags := applyValidate(t, b)
49+
require.Len(t, diags, 1)
50+
assert.Equal(t, diag.Warning, diags[0].Severity)
51+
assert.Contains(t, diags[0].Summary, "root path")
52+
assert.Contains(t, diags[0].Summary, "/Workspace/Shared/foo/bar")
53+
}
54+
55+
func TestValidateWorkspaceSharedPermissions_StateShared_WarnWithoutUsersManage(t *testing.T) {
56+
b := &bundle.Bundle{
57+
Config: config.Root{
58+
Workspace: config.Workspace{
59+
RootPath: "/Workspace/Users/user@example.test",
60+
StatePath: "/Workspace/Shared/state",
61+
},
62+
Permissions: []resources.Permission{
63+
{Level: CAN_MANAGE, UserName: "user@example.test"},
5464
},
5565
},
5666
}
67+
diags := applyValidate(t, b)
68+
require.Len(t, diags, 1)
69+
assert.Equal(t, diag.Warning, diags[0].Severity)
70+
assert.Contains(t, diags[0].Summary, "state path")
71+
assert.Contains(t, diags[0].Summary, "/Workspace/Shared/state")
72+
}
5773

58-
m := mocks.NewMockWorkspaceClient(t)
59-
b.SetWorkpaceClient(m.WorkspaceClient)
74+
func TestValidateWorkspaceSharedPermissions_StateShared_NoWarningWithUsersManage(t *testing.T) {
75+
b := &bundle.Bundle{
76+
Config: config.Root{
77+
Workspace: config.Workspace{
78+
RootPath: "/Workspace/Users/user@example.test",
79+
StatePath: "/Workspace/Shared/state",
80+
},
81+
Permissions: []resources.Permission{
82+
{Level: CAN_MANAGE, GroupName: "users"},
83+
},
84+
},
85+
}
86+
diags := applyValidate(t, b)
87+
require.Empty(t, diags)
88+
}
6089

61-
diags := bundle.Apply(t.Context(), b, ValidateSharedRootPermissions())
90+
func TestValidateWorkspaceSharedPermissions_StateUnderSharedRoot_OnlyRootWarning(t *testing.T) {
91+
// state_path is nested under root_path — the root warning already covers it,
92+
// so the state warning must not fire.
93+
b := &bundle.Bundle{
94+
Config: config.Root{
95+
Workspace: config.Workspace{
96+
RootPath: "/Workspace/Shared/bundle",
97+
StatePath: "/Workspace/Shared/bundle/state",
98+
},
99+
Permissions: []resources.Permission{
100+
{Level: CAN_MANAGE, UserName: "user@example.test"},
101+
},
102+
},
103+
}
104+
diags := applyValidate(t, b)
62105
require.Len(t, diags, 1)
63-
require.Equal(t, "the bundle root path /Workspace/Shared/foo/bar is writable by all workspace users", diags[0].Summary)
64-
require.Equal(t, diag.Warning, diags[0].Severity)
106+
assert.Contains(t, diags[0].Summary, "root path")
107+
}
108+
109+
func TestValidateWorkspaceSharedPermissions_BothSharedSeparateFolders_TwoWarnings(t *testing.T) {
110+
// root_path and state_path are both shared but state_path is not under root_path,
111+
// so both warnings fire.
112+
b := &bundle.Bundle{
113+
Config: config.Root{
114+
Workspace: config.Workspace{
115+
RootPath: "/Workspace/Shared/bundle",
116+
StatePath: "/Workspace/Shared/separate-state",
117+
},
118+
Permissions: []resources.Permission{
119+
{Level: CAN_MANAGE, UserName: "user@example.test"},
120+
},
121+
},
122+
}
123+
diags := applyValidate(t, b)
124+
require.Len(t, diags, 2)
125+
assert.Contains(t, diags[0].Summary, "root path")
126+
assert.Contains(t, diags[1].Summary, "state path")
127+
}
128+
129+
func TestValidateWorkspaceSharedPermissions_NoSharedPaths_NoWarning(t *testing.T) {
130+
b := &bundle.Bundle{
131+
Config: config.Root{
132+
Workspace: config.Workspace{
133+
RootPath: "/Workspace/Users/user@example.test/bundle",
134+
StatePath: "/Workspace/Users/user@example.test/other-state",
135+
},
136+
},
137+
}
138+
diags := applyValidate(t, b)
139+
require.Empty(t, diags)
65140
}

bundle/permissions/workspace_root_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ func TestApplyWorkspaceRootPermissions(t *testing.T) {
7272
WorkspaceObjectType: "directories",
7373
}).Return(nil, nil)
7474

75-
diags := bundle.ApplySeq(t.Context(), b, ValidateSharedRootPermissions(), ApplyWorkspaceRootPermissions())
75+
diags := bundle.ApplySeq(t.Context(), b, ValidateWorkspaceSharedPermissions(), ApplyWorkspaceRootPermissions())
7676
require.Empty(t, diags)
7777
}
7878

bundle/phases/initialize.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -191,10 +191,10 @@ func Initialize(ctx context.Context, b *bundle.Bundle) {
191191
apps.Validate(),
192192

193193
resourcemutator.ValidateTargetMode(),
194-
// Reads (typed): b.Config.Workspace.RootPath (checks if path is in shared workspace)
195-
// Reads (typed): b.Config.Permissions (checks if users group has CAN_MANAGE permission)
196-
// Validates that when using a shared workspace path, appropriate permissions are configured
197-
permissions.ValidateSharedRootPermissions(),
194+
// Reads (typed): b.Config.Workspace.{RootPath,StatePath} and b.Config.Permissions.
195+
// Warns when a workspace path is in /Workspace/Shared without the permissions
196+
// section granting all workspace users CAN_MANAGE.
197+
permissions.ValidateWorkspaceSharedPermissions(),
198198

199199
// Annotate resources with "deployment" metadata.
200200
//

0 commit comments

Comments
 (0)