Skip to content

Commit a18a97e

Browse files
authored
Added support for top-level permissions (#928)
## Changes Now it's possible to define top level `permissions` section in bundle configuration and permissions defined there will be applied to all resources defined in the bundle. Supported top-level permission levels: CAN_MANAGE, CAN_VIEW, CAN_RUN. Permissions are applied to: Jobs, DLT Pipelines, ML Models, ML Experiments and Model Service Endpoints ``` bundle: name: permissions workspace: host: *** permissions: - level: CAN_VIEW group_name: test-group - level: CAN_MANAGE user_name: user@company.com - level: CAN_RUN service_principal_name: 123456-abcdef ``` ## Tests Added corresponding unit tests + ran `bundle validate` and `bundle deploy` manually
1 parent ecf2835 commit a18a97e

11 files changed

Lines changed: 678 additions & 1 deletion

File tree

bundle/config/root.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"path/filepath"
77
"strings"
88

9+
"github.com/databricks/cli/bundle/config/resources"
910
"github.com/databricks/cli/bundle/config/variable"
1011
"github.com/databricks/databricks-sdk-go/service/jobs"
1112
"github.com/ghodss/yaml"
@@ -56,6 +57,10 @@ type Root struct {
5657
RunAs *jobs.JobRunAs `json:"run_as,omitempty"`
5758

5859
Experimental *Experimental `json:"experimental,omitempty"`
60+
61+
// Permissions section allows to define permissions which will be
62+
// applied to all resources defined in bundle
63+
Permissions []resources.Permission `json:"permissions,omitempty"`
5964
}
6065

6166
// Load loads the bundle configuration file at the specified path.
@@ -237,5 +242,12 @@ func (r *Root) MergeTargetOverrides(target *Target) error {
237242
}
238243
}
239244

245+
if target.Permissions != nil {
246+
err = mergo.Merge(&r.Permissions, target.Permissions, mergo.WithAppendSlice)
247+
if err != nil {
248+
return err
249+
}
250+
}
251+
240252
return nil
241253
}

bundle/config/target.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package config
22

3-
import "github.com/databricks/databricks-sdk-go/service/jobs"
3+
import (
4+
"github.com/databricks/cli/bundle/config/resources"
5+
"github.com/databricks/databricks-sdk-go/service/jobs"
6+
)
47

58
type Mode string
69

@@ -37,6 +40,8 @@ type Target struct {
3740
RunAs *jobs.JobRunAs `json:"run_as,omitempty"`
3841

3942
Sync *Sync `json:"sync,omitempty"`
43+
44+
Permissions []resources.Permission `json:"permissions,omitempty"`
4045
}
4146

4247
const (

bundle/permissions/mutator.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package permissions
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"slices"
7+
"strings"
8+
9+
"github.com/databricks/cli/bundle"
10+
)
11+
12+
const CAN_MANAGE = "CAN_MANAGE"
13+
const CAN_VIEW = "CAN_VIEW"
14+
const CAN_RUN = "CAN_RUN"
15+
16+
var allowedLevels = []string{CAN_MANAGE, CAN_VIEW, CAN_RUN}
17+
var levelsMap = map[string](map[string]string){
18+
"jobs": {
19+
CAN_MANAGE: "CAN_MANAGE",
20+
CAN_VIEW: "CAN_VIEW",
21+
CAN_RUN: "CAN_MANAGE_RUN",
22+
},
23+
"pipelines": {
24+
CAN_MANAGE: "CAN_MANAGE",
25+
CAN_VIEW: "CAN_VIEW",
26+
CAN_RUN: "CAN_RUN",
27+
},
28+
"mlflow_experiments": {
29+
CAN_MANAGE: "CAN_MANAGE",
30+
CAN_VIEW: "CAN_READ",
31+
},
32+
"mlflow_models": {
33+
CAN_MANAGE: "CAN_MANAGE",
34+
CAN_VIEW: "CAN_READ",
35+
},
36+
"model_serving_endpoints": {
37+
CAN_MANAGE: "CAN_MANAGE",
38+
CAN_VIEW: "CAN_VIEW",
39+
CAN_RUN: "CAN_QUERY",
40+
},
41+
}
42+
43+
type bundlePermissions struct{}
44+
45+
func ApplyBundlePermissions() bundle.Mutator {
46+
return &bundlePermissions{}
47+
}
48+
49+
func (m *bundlePermissions) Apply(ctx context.Context, b *bundle.Bundle) error {
50+
err := validate(b)
51+
if err != nil {
52+
return err
53+
}
54+
55+
applyForJobs(ctx, b)
56+
applyForPipelines(ctx, b)
57+
applyForMlModels(ctx, b)
58+
applyForMlExperiments(ctx, b)
59+
applyForModelServiceEndpoints(ctx, b)
60+
61+
return nil
62+
}
63+
64+
func validate(b *bundle.Bundle) error {
65+
for _, p := range b.Config.Permissions {
66+
if !slices.Contains(allowedLevels, p.Level) {
67+
return fmt.Errorf("invalid permission level: %s, allowed values: [%s]", p.Level, strings.Join(allowedLevels, ", "))
68+
}
69+
}
70+
71+
return nil
72+
}
73+
74+
func applyForJobs(ctx context.Context, b *bundle.Bundle) {
75+
for _, job := range b.Config.Resources.Jobs {
76+
job.Permissions = append(job.Permissions, convert(
77+
ctx,
78+
b.Config.Permissions,
79+
job.Permissions,
80+
job.Name,
81+
levelsMap["jobs"],
82+
)...)
83+
}
84+
}
85+
86+
func applyForPipelines(ctx context.Context, b *bundle.Bundle) {
87+
for _, pipeline := range b.Config.Resources.Pipelines {
88+
pipeline.Permissions = append(pipeline.Permissions, convert(
89+
ctx,
90+
b.Config.Permissions,
91+
pipeline.Permissions,
92+
pipeline.Name,
93+
levelsMap["pipelines"],
94+
)...)
95+
}
96+
}
97+
98+
func applyForMlExperiments(ctx context.Context, b *bundle.Bundle) {
99+
for _, experiment := range b.Config.Resources.Experiments {
100+
experiment.Permissions = append(experiment.Permissions, convert(
101+
ctx,
102+
b.Config.Permissions,
103+
experiment.Permissions,
104+
experiment.Name,
105+
levelsMap["mlflow_experiments"],
106+
)...)
107+
}
108+
}
109+
110+
func applyForMlModels(ctx context.Context, b *bundle.Bundle) {
111+
for _, model := range b.Config.Resources.Models {
112+
model.Permissions = append(model.Permissions, convert(
113+
ctx,
114+
b.Config.Permissions,
115+
model.Permissions,
116+
model.Name,
117+
levelsMap["mlflow_models"],
118+
)...)
119+
}
120+
}
121+
122+
func applyForModelServiceEndpoints(ctx context.Context, b *bundle.Bundle) {
123+
for _, model := range b.Config.Resources.ModelServingEndpoints {
124+
model.Permissions = append(model.Permissions, convert(
125+
ctx,
126+
b.Config.Permissions,
127+
model.Permissions,
128+
model.Name,
129+
levelsMap["model_serving_endpoints"],
130+
)...)
131+
}
132+
}
133+
134+
func (m *bundlePermissions) Name() string {
135+
return "ApplyBundlePermissions"
136+
}

bundle/permissions/mutator_test.go

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
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/jobs"
11+
"github.com/databricks/databricks-sdk-go/service/ml"
12+
"github.com/databricks/databricks-sdk-go/service/pipelines"
13+
"github.com/databricks/databricks-sdk-go/service/serving"
14+
"github.com/stretchr/testify/require"
15+
)
16+
17+
func TestApplyBundlePermissions(t *testing.T) {
18+
b := &bundle.Bundle{
19+
Config: config.Root{
20+
Workspace: config.Workspace{
21+
RootPath: "/Users/foo@bar.com",
22+
},
23+
Permissions: []resources.Permission{
24+
{Level: CAN_MANAGE, UserName: "TestUser"},
25+
{Level: CAN_VIEW, GroupName: "TestGroup"},
26+
{Level: CAN_RUN, ServicePrincipalName: "TestServicePrincipal"},
27+
},
28+
Resources: config.Resources{
29+
Jobs: map[string]*resources.Job{
30+
"job_1": {JobSettings: &jobs.JobSettings{}},
31+
"job_2": {JobSettings: &jobs.JobSettings{}},
32+
},
33+
Pipelines: map[string]*resources.Pipeline{
34+
"pipeline_1": {PipelineSpec: &pipelines.PipelineSpec{}},
35+
"pipeline_2": {PipelineSpec: &pipelines.PipelineSpec{}},
36+
},
37+
Models: map[string]*resources.MlflowModel{
38+
"model_1": {Model: &ml.Model{}},
39+
"model_2": {Model: &ml.Model{}},
40+
},
41+
Experiments: map[string]*resources.MlflowExperiment{
42+
"experiment_1": {Experiment: &ml.Experiment{}},
43+
"experiment_2": {Experiment: &ml.Experiment{}},
44+
},
45+
ModelServingEndpoints: map[string]*resources.ModelServingEndpoint{
46+
"endpoint_1": {CreateServingEndpoint: &serving.CreateServingEndpoint{}},
47+
"endpoint_2": {CreateServingEndpoint: &serving.CreateServingEndpoint{}},
48+
},
49+
},
50+
},
51+
}
52+
53+
err := bundle.Apply(context.Background(), b, ApplyBundlePermissions())
54+
require.NoError(t, err)
55+
56+
require.Len(t, b.Config.Resources.Jobs["job_1"].Permissions, 3)
57+
require.Contains(t, b.Config.Resources.Jobs["job_1"].Permissions, resources.Permission{Level: "CAN_MANAGE", UserName: "TestUser"})
58+
require.Contains(t, b.Config.Resources.Jobs["job_1"].Permissions, resources.Permission{Level: "CAN_VIEW", GroupName: "TestGroup"})
59+
require.Contains(t, b.Config.Resources.Jobs["job_1"].Permissions, resources.Permission{Level: "CAN_MANAGE_RUN", ServicePrincipalName: "TestServicePrincipal"})
60+
61+
require.Len(t, b.Config.Resources.Jobs["job_2"].Permissions, 3)
62+
require.Contains(t, b.Config.Resources.Jobs["job_2"].Permissions, resources.Permission{Level: "CAN_MANAGE", UserName: "TestUser"})
63+
require.Contains(t, b.Config.Resources.Jobs["job_2"].Permissions, resources.Permission{Level: "CAN_VIEW", GroupName: "TestGroup"})
64+
require.Contains(t, b.Config.Resources.Jobs["job_2"].Permissions, resources.Permission{Level: "CAN_MANAGE_RUN", ServicePrincipalName: "TestServicePrincipal"})
65+
66+
require.Len(t, b.Config.Resources.Pipelines["pipeline_1"].Permissions, 3)
67+
require.Contains(t, b.Config.Resources.Pipelines["pipeline_1"].Permissions, resources.Permission{Level: "CAN_MANAGE", UserName: "TestUser"})
68+
require.Contains(t, b.Config.Resources.Pipelines["pipeline_1"].Permissions, resources.Permission{Level: "CAN_VIEW", GroupName: "TestGroup"})
69+
require.Contains(t, b.Config.Resources.Pipelines["pipeline_1"].Permissions, resources.Permission{Level: "CAN_RUN", ServicePrincipalName: "TestServicePrincipal"})
70+
71+
require.Len(t, b.Config.Resources.Pipelines["pipeline_2"].Permissions, 3)
72+
require.Contains(t, b.Config.Resources.Pipelines["pipeline_2"].Permissions, resources.Permission{Level: "CAN_MANAGE", UserName: "TestUser"})
73+
require.Contains(t, b.Config.Resources.Pipelines["pipeline_2"].Permissions, resources.Permission{Level: "CAN_VIEW", GroupName: "TestGroup"})
74+
require.Contains(t, b.Config.Resources.Pipelines["pipeline_2"].Permissions, resources.Permission{Level: "CAN_RUN", ServicePrincipalName: "TestServicePrincipal"})
75+
76+
require.Len(t, b.Config.Resources.Models["model_1"].Permissions, 2)
77+
require.Contains(t, b.Config.Resources.Models["model_1"].Permissions, resources.Permission{Level: "CAN_MANAGE", UserName: "TestUser"})
78+
require.Contains(t, b.Config.Resources.Models["model_1"].Permissions, resources.Permission{Level: "CAN_READ", GroupName: "TestGroup"})
79+
80+
require.Len(t, b.Config.Resources.Models["model_2"].Permissions, 2)
81+
require.Contains(t, b.Config.Resources.Models["model_2"].Permissions, resources.Permission{Level: "CAN_MANAGE", UserName: "TestUser"})
82+
require.Contains(t, b.Config.Resources.Models["model_2"].Permissions, resources.Permission{Level: "CAN_READ", GroupName: "TestGroup"})
83+
84+
require.Len(t, b.Config.Resources.Experiments["experiment_1"].Permissions, 2)
85+
require.Contains(t, b.Config.Resources.Experiments["experiment_1"].Permissions, resources.Permission{Level: "CAN_MANAGE", UserName: "TestUser"})
86+
require.Contains(t, b.Config.Resources.Experiments["experiment_1"].Permissions, resources.Permission{Level: "CAN_READ", GroupName: "TestGroup"})
87+
88+
require.Len(t, b.Config.Resources.Experiments["experiment_2"].Permissions, 2)
89+
require.Contains(t, b.Config.Resources.Experiments["experiment_2"].Permissions, resources.Permission{Level: "CAN_MANAGE", UserName: "TestUser"})
90+
require.Contains(t, b.Config.Resources.Experiments["experiment_2"].Permissions, resources.Permission{Level: "CAN_READ", GroupName: "TestGroup"})
91+
92+
require.Len(t, b.Config.Resources.ModelServingEndpoints["endpoint_1"].Permissions, 3)
93+
require.Contains(t, b.Config.Resources.ModelServingEndpoints["endpoint_1"].Permissions, resources.Permission{Level: "CAN_MANAGE", UserName: "TestUser"})
94+
require.Contains(t, b.Config.Resources.ModelServingEndpoints["endpoint_1"].Permissions, resources.Permission{Level: "CAN_VIEW", GroupName: "TestGroup"})
95+
require.Contains(t, b.Config.Resources.ModelServingEndpoints["endpoint_1"].Permissions, resources.Permission{Level: "CAN_QUERY", ServicePrincipalName: "TestServicePrincipal"})
96+
97+
require.Len(t, b.Config.Resources.ModelServingEndpoints["endpoint_2"].Permissions, 3)
98+
require.Contains(t, b.Config.Resources.ModelServingEndpoints["endpoint_2"].Permissions, resources.Permission{Level: "CAN_MANAGE", UserName: "TestUser"})
99+
require.Contains(t, b.Config.Resources.ModelServingEndpoints["endpoint_2"].Permissions, resources.Permission{Level: "CAN_VIEW", GroupName: "TestGroup"})
100+
require.Contains(t, b.Config.Resources.ModelServingEndpoints["endpoint_2"].Permissions, resources.Permission{Level: "CAN_QUERY", ServicePrincipalName: "TestServicePrincipal"})
101+
}
102+
103+
func TestWarningOnOverlapPermission(t *testing.T) {
104+
b := &bundle.Bundle{
105+
Config: config.Root{
106+
Workspace: config.Workspace{
107+
RootPath: "/Users/foo@bar.com",
108+
},
109+
Permissions: []resources.Permission{
110+
{Level: CAN_MANAGE, UserName: "TestUser"},
111+
{Level: CAN_VIEW, GroupName: "TestGroup"},
112+
},
113+
Resources: config.Resources{
114+
Jobs: map[string]*resources.Job{
115+
"job_1": {
116+
Permissions: []resources.Permission{
117+
{Level: CAN_VIEW, UserName: "TestUser"},
118+
},
119+
JobSettings: &jobs.JobSettings{},
120+
},
121+
"job_2": {
122+
Permissions: []resources.Permission{
123+
{Level: CAN_VIEW, UserName: "TestUser2"},
124+
},
125+
JobSettings: &jobs.JobSettings{},
126+
},
127+
},
128+
},
129+
},
130+
}
131+
132+
err := bundle.Apply(context.Background(), b, ApplyBundlePermissions())
133+
require.NoError(t, err)
134+
135+
require.Contains(t, b.Config.Resources.Jobs["job_1"].Permissions, resources.Permission{Level: "CAN_VIEW", UserName: "TestUser"})
136+
require.Contains(t, b.Config.Resources.Jobs["job_1"].Permissions, resources.Permission{Level: "CAN_VIEW", GroupName: "TestGroup"})
137+
require.Contains(t, b.Config.Resources.Jobs["job_2"].Permissions, resources.Permission{Level: "CAN_VIEW", UserName: "TestUser2"})
138+
require.Contains(t, b.Config.Resources.Jobs["job_2"].Permissions, resources.Permission{Level: "CAN_MANAGE", UserName: "TestUser"})
139+
require.Contains(t, b.Config.Resources.Jobs["job_2"].Permissions, resources.Permission{Level: "CAN_VIEW", GroupName: "TestGroup"})
140+
141+
}

0 commit comments

Comments
 (0)