Skip to content

Commit

Permalink
Filter current user from resource permissions (#1262)
Browse files Browse the repository at this point in the history
## 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 at deploy time, 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
```

This PR is a reincarnation of
#1145. The earlier attempt had to
be reverted due to metadata loss converting to and from the dynamic
configuration representation (reverted here:
#1179)

## Tests
Unit test and manually
  • Loading branch information
shreyas-goenka authored Mar 11, 2024
1 parent 49a87ec commit d5dc2bd
Show file tree
Hide file tree
Showing 3 changed files with 255 additions and 0 deletions.
80 changes: 80 additions & 0 deletions bundle/permissions/filter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package permissions

import (
"context"

"github.com/databricks/cli/bundle"
"github.com/databricks/cli/libs/dyn"
)

type filterCurrentUser struct{}

// The databricks terraform provider does not allow changing the permissions of
// current user. The current user is implied to be the owner of all deployed resources.
// This mutator removes the current user from the permissions of all resources.
func FilterCurrentUser() bundle.Mutator {
return &filterCurrentUser{}
}

func (m *filterCurrentUser) Name() string {
return "FilterCurrentUserFromPermissions"
}

func filter(currentUser string) dyn.WalkValueFunc {
return func(p dyn.Path, v dyn.Value) (dyn.Value, error) {
// Permissions are defined at top level of a resource. We can skip walking
// after a depth of 4.
// [resource_type].[resource_name].[permissions].[array_index]
// Example: pipelines.foo.permissions.0
if len(p) > 4 {
return v, dyn.ErrSkip
}

// We can skip walking at a depth of 3 if the key is not "permissions".
// Example: pipelines.foo.libraries
if len(p) == 3 && p[2] != dyn.Key("permissions") {
return v, dyn.ErrSkip
}

// We want to be at the level of an individual permission to check it's
// user_name and service_principal_name fields.
if len(p) != 4 || p[2] != dyn.Key("permissions") {
return v, nil
}

// Filter if the user_name matches the current user
userName, ok := v.Get("user_name").AsString()
if ok && userName == currentUser {
return v, dyn.ErrDrop
}

// Filter if the service_principal_name matches the current user
servicePrincipalName, ok := v.Get("service_principal_name").AsString()
if ok && servicePrincipalName == currentUser {
return v, dyn.ErrDrop
}

return v, nil

}
}

func (m *filterCurrentUser) Apply(ctx context.Context, b *bundle.Bundle) error {
currentUser := b.Config.Workspace.CurrentUser.UserName

return b.Config.Mutate(func(v dyn.Value) (dyn.Value, error) {
rv, err := dyn.Get(v, "resources")
if err != nil {
return dyn.InvalidValue, err
}

// Walk the resources and filter out the current user from the permissions
nv, err := dyn.Walk(rv, filter(currentUser))
if err != nil {
return dyn.InvalidValue, err
}

// Set the resources with the filtered permissions back into the bundle
return dyn.Set(v, "resources", nv)
})
}
174 changes: 174 additions & 0 deletions bundle/permissions/filter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package permissions

import (
"context"
"testing"

"github.com/databricks/cli/bundle"
"github.com/databricks/cli/bundle/config"
"github.com/databricks/cli/bundle/config/resources"
"github.com/databricks/databricks-sdk-go/service/iam"
"github.com/stretchr/testify/assert"
)

var alice = resources.Permission{
Level: CAN_MANAGE,
UserName: "alice@databricks.com",
}

var bob = resources.Permission{
Level: CAN_VIEW,
UserName: "bob@databricks.com",
}

var robot = resources.Permission{
Level: CAN_RUN,
ServicePrincipalName: "i-Robot",
}

func testFixture(userName string) *bundle.Bundle {
p := []resources.Permission{
alice,
bob,
robot,
}

return &bundle.Bundle{
Config: config.Root{
Workspace: config.Workspace{
CurrentUser: &config.User{
User: &iam.User{
UserName: userName,
},
},
},
Resources: config.Resources{
Jobs: map[string]*resources.Job{
"job1": {
Permissions: p,
},
"job2": {
Permissions: p,
},
},
Pipelines: map[string]*resources.Pipeline{
"pipeline1": {
Permissions: p,
},
},
Experiments: map[string]*resources.MlflowExperiment{
"experiment1": {
Permissions: p,
},
},
Models: map[string]*resources.MlflowModel{
"model1": {
Permissions: p,
},
},
ModelServingEndpoints: map[string]*resources.ModelServingEndpoint{
"endpoint1": {
Permissions: p,
},
},
RegisteredModels: map[string]*resources.RegisteredModel{
"registered_model1": {
Grants: []resources.Grant{
{
Principal: "abc",
},
},
},
},
},
},
}

}

func TestFilterCurrentUser(t *testing.T) {
b := testFixture("alice@databricks.com")

err := bundle.Apply(context.Background(), b, FilterCurrentUser())
assert.NoError(t, err)

// Assert current user is filtered out.
assert.Equal(t, 2, len(b.Config.Resources.Jobs["job1"].Permissions))
assert.Contains(t, b.Config.Resources.Jobs["job1"].Permissions, robot)
assert.Contains(t, b.Config.Resources.Jobs["job1"].Permissions, bob)

assert.Equal(t, 2, len(b.Config.Resources.Jobs["job2"].Permissions))
assert.Contains(t, b.Config.Resources.Jobs["job2"].Permissions, robot)
assert.Contains(t, b.Config.Resources.Jobs["job2"].Permissions, bob)

assert.Equal(t, 2, len(b.Config.Resources.Pipelines["pipeline1"].Permissions))
assert.Contains(t, b.Config.Resources.Pipelines["pipeline1"].Permissions, robot)
assert.Contains(t, b.Config.Resources.Pipelines["pipeline1"].Permissions, bob)

assert.Equal(t, 2, len(b.Config.Resources.Experiments["experiment1"].Permissions))
assert.Contains(t, b.Config.Resources.Experiments["experiment1"].Permissions, robot)
assert.Contains(t, b.Config.Resources.Experiments["experiment1"].Permissions, bob)

assert.Equal(t, 2, len(b.Config.Resources.Models["model1"].Permissions))
assert.Contains(t, b.Config.Resources.Models["model1"].Permissions, robot)
assert.Contains(t, b.Config.Resources.Models["model1"].Permissions, bob)

assert.Equal(t, 2, len(b.Config.Resources.ModelServingEndpoints["endpoint1"].Permissions))
assert.Contains(t, b.Config.Resources.ModelServingEndpoints["endpoint1"].Permissions, robot)
assert.Contains(t, b.Config.Resources.ModelServingEndpoints["endpoint1"].Permissions, bob)

// Assert there's no change to the grant.
assert.Equal(t, 1, len(b.Config.Resources.RegisteredModels["registered_model1"].Grants))
}

func TestFilterCurrentServicePrincipal(t *testing.T) {
b := testFixture("i-Robot")

err := bundle.Apply(context.Background(), b, FilterCurrentUser())
assert.NoError(t, err)

// Assert current user is filtered out.
assert.Equal(t, 2, len(b.Config.Resources.Jobs["job1"].Permissions))
assert.Contains(t, b.Config.Resources.Jobs["job1"].Permissions, alice)
assert.Contains(t, b.Config.Resources.Jobs["job1"].Permissions, bob)

assert.Equal(t, 2, len(b.Config.Resources.Jobs["job2"].Permissions))
assert.Contains(t, b.Config.Resources.Jobs["job2"].Permissions, alice)
assert.Contains(t, b.Config.Resources.Jobs["job2"].Permissions, bob)

assert.Equal(t, 2, len(b.Config.Resources.Pipelines["pipeline1"].Permissions))
assert.Contains(t, b.Config.Resources.Pipelines["pipeline1"].Permissions, alice)
assert.Contains(t, b.Config.Resources.Pipelines["pipeline1"].Permissions, bob)

assert.Equal(t, 2, len(b.Config.Resources.Experiments["experiment1"].Permissions))
assert.Contains(t, b.Config.Resources.Experiments["experiment1"].Permissions, alice)
assert.Contains(t, b.Config.Resources.Experiments["experiment1"].Permissions, bob)

assert.Equal(t, 2, len(b.Config.Resources.Models["model1"].Permissions))
assert.Contains(t, b.Config.Resources.Models["model1"].Permissions, alice)
assert.Contains(t, b.Config.Resources.Models["model1"].Permissions, bob)

assert.Equal(t, 2, len(b.Config.Resources.ModelServingEndpoints["endpoint1"].Permissions))
assert.Contains(t, b.Config.Resources.ModelServingEndpoints["endpoint1"].Permissions, alice)
assert.Contains(t, b.Config.Resources.ModelServingEndpoints["endpoint1"].Permissions, bob)

// Assert there's no change to the grant.
assert.Equal(t, 1, len(b.Config.Resources.RegisteredModels["registered_model1"].Grants))
}

func TestFilterCurrentUserDoesNotErrorWhenNoResources(t *testing.T) {
b := &bundle.Bundle{
Config: config.Root{
Workspace: config.Workspace{
CurrentUser: &config.User{
User: &iam.User{
UserName: "abc",
},
},
},
},
}

err := bundle.Apply(context.Background(), b, FilterCurrentUser())
assert.NoError(t, err)
}
1 change: 1 addition & 0 deletions bundle/phases/initialize.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ func Initialize() bundle.Mutator {
mutator.TranslatePaths(),
python.WrapperWarning(),
permissions.ApplyBundlePermissions(),
permissions.FilterCurrentUser(),
metadata.AnnotateJobs(),
terraform.Initialize(),
scripts.Execute(config.ScriptPostInit),
Expand Down

0 comments on commit d5dc2bd

Please sign in to comment.