Skip to content

Commit 4131069

Browse files
Filter current user from resource permissions (#1145)
## Changes The databricks terraform provider does not allow changing permission of the current user. Instead, the current identity is implictly set to be the owner of all resources on the platform side. This PR introduces a mutator to filter permissions from the bundle configuration, allowing users to define permissions for their own identities in their bundle config. This would allow configurations like, allowing both alice and bob to collaborate on the same DAB: ``` permissions: level: CAN_MANAGE user_name: alice level: CAN_MANAGE user_name: bob ``` ## Tests Unit test and manually
1 parent 20e45b8 commit 4131069

File tree

3 files changed

+238
-0
lines changed

3 files changed

+238
-0
lines changed

bundle/permissions/filter.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package permissions
2+
3+
import (
4+
"context"
5+
6+
"github.com/databricks/cli/bundle"
7+
"github.com/databricks/cli/libs/dyn"
8+
dync "github.com/databricks/cli/libs/dyn/convert"
9+
)
10+
11+
type filterCurrentUser struct{}
12+
13+
// The databricks terraform provider does not allow changing the permissions of
14+
// current user. The current user is implied to be the owner of all deployed resources.
15+
// This mutator removes the current user from the permissions of all resources.
16+
func FilterCurrentUser() bundle.Mutator {
17+
return &filterCurrentUser{}
18+
}
19+
20+
func (m *filterCurrentUser) Name() string {
21+
return "FilterCurrentUserFromPermissions"
22+
}
23+
24+
func filter(currentUser string) dyn.WalkValueFunc {
25+
return func(p dyn.Path, v dyn.Value) (dyn.Value, error) {
26+
// Permissions are defined at top level of a resource. We can skip walking
27+
// after a depth of 4.
28+
// [resource_type].[resource_name].[permissions].[array_index]
29+
// Example: pipelines.foo.permissions.0
30+
if len(p) > 4 {
31+
return v, dyn.ErrSkip
32+
}
33+
34+
// We can skip walking at a depth of 3 if the key is not "permissions".
35+
// Example: pipelines.foo.libraries
36+
if len(p) == 3 && p[2] != dyn.Key("permissions") {
37+
return v, dyn.ErrSkip
38+
}
39+
40+
// We want to be at the level of an individual permission to check it's
41+
// user_name and service_principal_name fields.
42+
if len(p) != 4 || p[2] != dyn.Key("permissions") {
43+
return v, nil
44+
}
45+
46+
// Filter if the user_name matches the current user
47+
userName, ok := v.Get("user_name").AsString()
48+
if ok && userName == currentUser {
49+
return v, dyn.ErrDrop
50+
}
51+
52+
// Filter if the service_principal_name matches the current user
53+
servicePrincipalName, ok := v.Get("service_principal_name").AsString()
54+
if ok && servicePrincipalName == currentUser {
55+
return v, dyn.ErrDrop
56+
}
57+
58+
return v, nil
59+
}
60+
}
61+
62+
func (m *filterCurrentUser) Apply(ctx context.Context, b *bundle.Bundle) error {
63+
rv, err := dync.FromTyped(b.Config.Resources, dyn.NilValue)
64+
if err != nil {
65+
return err
66+
}
67+
68+
currentUser := b.Config.Workspace.CurrentUser.UserName
69+
nv, err := dyn.Walk(rv, filter(currentUser))
70+
if err != nil {
71+
return err
72+
}
73+
74+
err = dync.ToTyped(&b.Config.Resources, nv)
75+
if err != nil {
76+
return err
77+
}
78+
79+
return nil
80+
}

bundle/permissions/filter_test.go

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package permissions
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/databricks/cli/bundle"
8+
"github.com/databricks/cli/bundle/config"
9+
"github.com/databricks/cli/bundle/config/resources"
10+
"github.com/databricks/databricks-sdk-go/service/iam"
11+
"github.com/stretchr/testify/assert"
12+
)
13+
14+
var alice = resources.Permission{
15+
Level: CAN_MANAGE,
16+
UserName: "[email protected]",
17+
}
18+
19+
var bob = resources.Permission{
20+
Level: CAN_VIEW,
21+
UserName: "[email protected]",
22+
}
23+
24+
var robot = resources.Permission{
25+
Level: CAN_RUN,
26+
ServicePrincipalName: "i-Robot",
27+
}
28+
29+
func testFixture(userName string) *bundle.Bundle {
30+
p := []resources.Permission{
31+
alice,
32+
bob,
33+
robot,
34+
}
35+
36+
return &bundle.Bundle{
37+
Config: config.Root{
38+
Workspace: config.Workspace{
39+
CurrentUser: &config.User{
40+
User: &iam.User{
41+
UserName: userName,
42+
},
43+
},
44+
},
45+
Resources: config.Resources{
46+
Jobs: map[string]*resources.Job{
47+
"job1": {
48+
Permissions: p,
49+
},
50+
"job2": {
51+
Permissions: p,
52+
},
53+
},
54+
Pipelines: map[string]*resources.Pipeline{
55+
"pipeline1": {
56+
Permissions: p,
57+
},
58+
},
59+
Experiments: map[string]*resources.MlflowExperiment{
60+
"experiment1": {
61+
Permissions: p,
62+
},
63+
},
64+
Models: map[string]*resources.MlflowModel{
65+
"model1": {
66+
Permissions: p,
67+
},
68+
},
69+
ModelServingEndpoints: map[string]*resources.ModelServingEndpoint{
70+
"endpoint1": {
71+
Permissions: p,
72+
},
73+
},
74+
RegisteredModels: map[string]*resources.RegisteredModel{
75+
"registered_model1": {
76+
Grants: []resources.Grant{
77+
{
78+
Principal: "abc",
79+
},
80+
},
81+
},
82+
},
83+
},
84+
},
85+
}
86+
87+
}
88+
89+
func TestFilterCurrentUser(t *testing.T) {
90+
b := testFixture("[email protected]")
91+
92+
err := bundle.Apply(context.Background(), b, FilterCurrentUser())
93+
assert.NoError(t, err)
94+
95+
// Assert current user is filtered out.
96+
assert.Equal(t, 2, len(b.Config.Resources.Jobs["job1"].Permissions))
97+
assert.Contains(t, b.Config.Resources.Jobs["job1"].Permissions, robot)
98+
assert.Contains(t, b.Config.Resources.Jobs["job1"].Permissions, bob)
99+
100+
assert.Equal(t, 2, len(b.Config.Resources.Jobs["job2"].Permissions))
101+
assert.Contains(t, b.Config.Resources.Jobs["job2"].Permissions, robot)
102+
assert.Contains(t, b.Config.Resources.Jobs["job2"].Permissions, bob)
103+
104+
assert.Equal(t, 2, len(b.Config.Resources.Pipelines["pipeline1"].Permissions))
105+
assert.Contains(t, b.Config.Resources.Pipelines["pipeline1"].Permissions, robot)
106+
assert.Contains(t, b.Config.Resources.Pipelines["pipeline1"].Permissions, bob)
107+
108+
assert.Equal(t, 2, len(b.Config.Resources.Experiments["experiment1"].Permissions))
109+
assert.Contains(t, b.Config.Resources.Experiments["experiment1"].Permissions, robot)
110+
assert.Contains(t, b.Config.Resources.Experiments["experiment1"].Permissions, bob)
111+
112+
assert.Equal(t, 2, len(b.Config.Resources.Models["model1"].Permissions))
113+
assert.Contains(t, b.Config.Resources.Models["model1"].Permissions, robot)
114+
assert.Contains(t, b.Config.Resources.Models["model1"].Permissions, bob)
115+
116+
assert.Equal(t, 2, len(b.Config.Resources.ModelServingEndpoints["endpoint1"].Permissions))
117+
assert.Contains(t, b.Config.Resources.ModelServingEndpoints["endpoint1"].Permissions, robot)
118+
assert.Contains(t, b.Config.Resources.ModelServingEndpoints["endpoint1"].Permissions, bob)
119+
120+
// Assert there's no change to the grant.
121+
assert.Equal(t, 1, len(b.Config.Resources.RegisteredModels["registered_model1"].Grants))
122+
}
123+
124+
func TestFilterCurrentServicePrincipal(t *testing.T) {
125+
b := testFixture("i-Robot")
126+
127+
err := bundle.Apply(context.Background(), b, FilterCurrentUser())
128+
assert.NoError(t, err)
129+
130+
// Assert current user is filtered out.
131+
assert.Equal(t, 2, len(b.Config.Resources.Jobs["job1"].Permissions))
132+
assert.Contains(t, b.Config.Resources.Jobs["job1"].Permissions, alice)
133+
assert.Contains(t, b.Config.Resources.Jobs["job1"].Permissions, bob)
134+
135+
assert.Equal(t, 2, len(b.Config.Resources.Jobs["job2"].Permissions))
136+
assert.Contains(t, b.Config.Resources.Jobs["job2"].Permissions, alice)
137+
assert.Contains(t, b.Config.Resources.Jobs["job2"].Permissions, bob)
138+
139+
assert.Equal(t, 2, len(b.Config.Resources.Pipelines["pipeline1"].Permissions))
140+
assert.Contains(t, b.Config.Resources.Pipelines["pipeline1"].Permissions, alice)
141+
assert.Contains(t, b.Config.Resources.Pipelines["pipeline1"].Permissions, bob)
142+
143+
assert.Equal(t, 2, len(b.Config.Resources.Experiments["experiment1"].Permissions))
144+
assert.Contains(t, b.Config.Resources.Experiments["experiment1"].Permissions, alice)
145+
assert.Contains(t, b.Config.Resources.Experiments["experiment1"].Permissions, bob)
146+
147+
assert.Equal(t, 2, len(b.Config.Resources.Models["model1"].Permissions))
148+
assert.Contains(t, b.Config.Resources.Models["model1"].Permissions, alice)
149+
assert.Contains(t, b.Config.Resources.Models["model1"].Permissions, bob)
150+
151+
assert.Equal(t, 2, len(b.Config.Resources.ModelServingEndpoints["endpoint1"].Permissions))
152+
assert.Contains(t, b.Config.Resources.ModelServingEndpoints["endpoint1"].Permissions, alice)
153+
assert.Contains(t, b.Config.Resources.ModelServingEndpoints["endpoint1"].Permissions, bob)
154+
155+
// Assert there's no change to the grant.
156+
assert.Equal(t, 1, len(b.Config.Resources.RegisteredModels["registered_model1"].Grants))
157+
}

bundle/phases/initialize.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ func Initialize() bundle.Mutator {
3939
mutator.TranslatePaths(),
4040
python.WrapperWarning(),
4141
permissions.ApplyBundlePermissions(),
42+
permissions.FilterCurrentUser(),
4243
metadata.AnnotateJobs(),
4344
terraform.Initialize(),
4445
scripts.Execute(config.ScriptPostInit),

0 commit comments

Comments
 (0)