mirror of https://github.com/databricks/cli.git
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
This commit is contained in:
parent
14d2d0a2d5
commit
f3db42e622
|
@ -6,6 +6,7 @@ import (
|
|||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/databricks/cli/bundle/config/resources"
|
||||
"github.com/databricks/cli/bundle/config/variable"
|
||||
"github.com/databricks/databricks-sdk-go/service/jobs"
|
||||
"github.com/ghodss/yaml"
|
||||
|
@ -56,6 +57,10 @@ type Root struct {
|
|||
RunAs *jobs.JobRunAs `json:"run_as,omitempty"`
|
||||
|
||||
Experimental *Experimental `json:"experimental,omitempty"`
|
||||
|
||||
// Permissions section allows to define permissions which will be
|
||||
// applied to all resources defined in bundle
|
||||
Permissions []resources.Permission `json:"permissions,omitempty"`
|
||||
}
|
||||
|
||||
// Load loads the bundle configuration file at the specified path.
|
||||
|
@ -237,5 +242,12 @@ func (r *Root) MergeTargetOverrides(target *Target) error {
|
|||
}
|
||||
}
|
||||
|
||||
if target.Permissions != nil {
|
||||
err = mergo.Merge(&r.Permissions, target.Permissions, mergo.WithAppendSlice)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package config
|
||||
|
||||
import "github.com/databricks/databricks-sdk-go/service/jobs"
|
||||
import (
|
||||
"github.com/databricks/cli/bundle/config/resources"
|
||||
"github.com/databricks/databricks-sdk-go/service/jobs"
|
||||
)
|
||||
|
||||
type Mode string
|
||||
|
||||
|
@ -37,6 +40,8 @@ type Target struct {
|
|||
RunAs *jobs.JobRunAs `json:"run_as,omitempty"`
|
||||
|
||||
Sync *Sync `json:"sync,omitempty"`
|
||||
|
||||
Permissions []resources.Permission `json:"permissions,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
|
|
|
@ -0,0 +1,136 @@
|
|||
package permissions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/databricks/cli/bundle"
|
||||
)
|
||||
|
||||
const CAN_MANAGE = "CAN_MANAGE"
|
||||
const CAN_VIEW = "CAN_VIEW"
|
||||
const CAN_RUN = "CAN_RUN"
|
||||
|
||||
var allowedLevels = []string{CAN_MANAGE, CAN_VIEW, CAN_RUN}
|
||||
var levelsMap = map[string](map[string]string){
|
||||
"jobs": {
|
||||
CAN_MANAGE: "CAN_MANAGE",
|
||||
CAN_VIEW: "CAN_VIEW",
|
||||
CAN_RUN: "CAN_MANAGE_RUN",
|
||||
},
|
||||
"pipelines": {
|
||||
CAN_MANAGE: "CAN_MANAGE",
|
||||
CAN_VIEW: "CAN_VIEW",
|
||||
CAN_RUN: "CAN_RUN",
|
||||
},
|
||||
"mlflow_experiments": {
|
||||
CAN_MANAGE: "CAN_MANAGE",
|
||||
CAN_VIEW: "CAN_READ",
|
||||
},
|
||||
"mlflow_models": {
|
||||
CAN_MANAGE: "CAN_MANAGE",
|
||||
CAN_VIEW: "CAN_READ",
|
||||
},
|
||||
"model_serving_endpoints": {
|
||||
CAN_MANAGE: "CAN_MANAGE",
|
||||
CAN_VIEW: "CAN_VIEW",
|
||||
CAN_RUN: "CAN_QUERY",
|
||||
},
|
||||
}
|
||||
|
||||
type bundlePermissions struct{}
|
||||
|
||||
func ApplyBundlePermissions() bundle.Mutator {
|
||||
return &bundlePermissions{}
|
||||
}
|
||||
|
||||
func (m *bundlePermissions) Apply(ctx context.Context, b *bundle.Bundle) error {
|
||||
err := validate(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
applyForJobs(ctx, b)
|
||||
applyForPipelines(ctx, b)
|
||||
applyForMlModels(ctx, b)
|
||||
applyForMlExperiments(ctx, b)
|
||||
applyForModelServiceEndpoints(ctx, b)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validate(b *bundle.Bundle) error {
|
||||
for _, p := range b.Config.Permissions {
|
||||
if !slices.Contains(allowedLevels, p.Level) {
|
||||
return fmt.Errorf("invalid permission level: %s, allowed values: [%s]", p.Level, strings.Join(allowedLevels, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func applyForJobs(ctx context.Context, b *bundle.Bundle) {
|
||||
for _, job := range b.Config.Resources.Jobs {
|
||||
job.Permissions = append(job.Permissions, convert(
|
||||
ctx,
|
||||
b.Config.Permissions,
|
||||
job.Permissions,
|
||||
job.Name,
|
||||
levelsMap["jobs"],
|
||||
)...)
|
||||
}
|
||||
}
|
||||
|
||||
func applyForPipelines(ctx context.Context, b *bundle.Bundle) {
|
||||
for _, pipeline := range b.Config.Resources.Pipelines {
|
||||
pipeline.Permissions = append(pipeline.Permissions, convert(
|
||||
ctx,
|
||||
b.Config.Permissions,
|
||||
pipeline.Permissions,
|
||||
pipeline.Name,
|
||||
levelsMap["pipelines"],
|
||||
)...)
|
||||
}
|
||||
}
|
||||
|
||||
func applyForMlExperiments(ctx context.Context, b *bundle.Bundle) {
|
||||
for _, experiment := range b.Config.Resources.Experiments {
|
||||
experiment.Permissions = append(experiment.Permissions, convert(
|
||||
ctx,
|
||||
b.Config.Permissions,
|
||||
experiment.Permissions,
|
||||
experiment.Name,
|
||||
levelsMap["mlflow_experiments"],
|
||||
)...)
|
||||
}
|
||||
}
|
||||
|
||||
func applyForMlModels(ctx context.Context, b *bundle.Bundle) {
|
||||
for _, model := range b.Config.Resources.Models {
|
||||
model.Permissions = append(model.Permissions, convert(
|
||||
ctx,
|
||||
b.Config.Permissions,
|
||||
model.Permissions,
|
||||
model.Name,
|
||||
levelsMap["mlflow_models"],
|
||||
)...)
|
||||
}
|
||||
}
|
||||
|
||||
func applyForModelServiceEndpoints(ctx context.Context, b *bundle.Bundle) {
|
||||
for _, model := range b.Config.Resources.ModelServingEndpoints {
|
||||
model.Permissions = append(model.Permissions, convert(
|
||||
ctx,
|
||||
b.Config.Permissions,
|
||||
model.Permissions,
|
||||
model.Name,
|
||||
levelsMap["model_serving_endpoints"],
|
||||
)...)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *bundlePermissions) Name() string {
|
||||
return "ApplyBundlePermissions"
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
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/jobs"
|
||||
"github.com/databricks/databricks-sdk-go/service/ml"
|
||||
"github.com/databricks/databricks-sdk-go/service/pipelines"
|
||||
"github.com/databricks/databricks-sdk-go/service/serving"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestApplyBundlePermissions(t *testing.T) {
|
||||
b := &bundle.Bundle{
|
||||
Config: config.Root{
|
||||
Workspace: config.Workspace{
|
||||
RootPath: "/Users/foo@bar.com",
|
||||
},
|
||||
Permissions: []resources.Permission{
|
||||
{Level: CAN_MANAGE, UserName: "TestUser"},
|
||||
{Level: CAN_VIEW, GroupName: "TestGroup"},
|
||||
{Level: CAN_RUN, ServicePrincipalName: "TestServicePrincipal"},
|
||||
},
|
||||
Resources: config.Resources{
|
||||
Jobs: map[string]*resources.Job{
|
||||
"job_1": {JobSettings: &jobs.JobSettings{}},
|
||||
"job_2": {JobSettings: &jobs.JobSettings{}},
|
||||
},
|
||||
Pipelines: map[string]*resources.Pipeline{
|
||||
"pipeline_1": {PipelineSpec: &pipelines.PipelineSpec{}},
|
||||
"pipeline_2": {PipelineSpec: &pipelines.PipelineSpec{}},
|
||||
},
|
||||
Models: map[string]*resources.MlflowModel{
|
||||
"model_1": {Model: &ml.Model{}},
|
||||
"model_2": {Model: &ml.Model{}},
|
||||
},
|
||||
Experiments: map[string]*resources.MlflowExperiment{
|
||||
"experiment_1": {Experiment: &ml.Experiment{}},
|
||||
"experiment_2": {Experiment: &ml.Experiment{}},
|
||||
},
|
||||
ModelServingEndpoints: map[string]*resources.ModelServingEndpoint{
|
||||
"endpoint_1": {CreateServingEndpoint: &serving.CreateServingEndpoint{}},
|
||||
"endpoint_2": {CreateServingEndpoint: &serving.CreateServingEndpoint{}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := bundle.Apply(context.Background(), b, ApplyBundlePermissions())
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, b.Config.Resources.Jobs["job_1"].Permissions, 3)
|
||||
require.Contains(t, b.Config.Resources.Jobs["job_1"].Permissions, resources.Permission{Level: "CAN_MANAGE", UserName: "TestUser"})
|
||||
require.Contains(t, b.Config.Resources.Jobs["job_1"].Permissions, resources.Permission{Level: "CAN_VIEW", GroupName: "TestGroup"})
|
||||
require.Contains(t, b.Config.Resources.Jobs["job_1"].Permissions, resources.Permission{Level: "CAN_MANAGE_RUN", ServicePrincipalName: "TestServicePrincipal"})
|
||||
|
||||
require.Len(t, b.Config.Resources.Jobs["job_2"].Permissions, 3)
|
||||
require.Contains(t, b.Config.Resources.Jobs["job_2"].Permissions, resources.Permission{Level: "CAN_MANAGE", UserName: "TestUser"})
|
||||
require.Contains(t, b.Config.Resources.Jobs["job_2"].Permissions, resources.Permission{Level: "CAN_VIEW", GroupName: "TestGroup"})
|
||||
require.Contains(t, b.Config.Resources.Jobs["job_2"].Permissions, resources.Permission{Level: "CAN_MANAGE_RUN", ServicePrincipalName: "TestServicePrincipal"})
|
||||
|
||||
require.Len(t, b.Config.Resources.Pipelines["pipeline_1"].Permissions, 3)
|
||||
require.Contains(t, b.Config.Resources.Pipelines["pipeline_1"].Permissions, resources.Permission{Level: "CAN_MANAGE", UserName: "TestUser"})
|
||||
require.Contains(t, b.Config.Resources.Pipelines["pipeline_1"].Permissions, resources.Permission{Level: "CAN_VIEW", GroupName: "TestGroup"})
|
||||
require.Contains(t, b.Config.Resources.Pipelines["pipeline_1"].Permissions, resources.Permission{Level: "CAN_RUN", ServicePrincipalName: "TestServicePrincipal"})
|
||||
|
||||
require.Len(t, b.Config.Resources.Pipelines["pipeline_2"].Permissions, 3)
|
||||
require.Contains(t, b.Config.Resources.Pipelines["pipeline_2"].Permissions, resources.Permission{Level: "CAN_MANAGE", UserName: "TestUser"})
|
||||
require.Contains(t, b.Config.Resources.Pipelines["pipeline_2"].Permissions, resources.Permission{Level: "CAN_VIEW", GroupName: "TestGroup"})
|
||||
require.Contains(t, b.Config.Resources.Pipelines["pipeline_2"].Permissions, resources.Permission{Level: "CAN_RUN", ServicePrincipalName: "TestServicePrincipal"})
|
||||
|
||||
require.Len(t, b.Config.Resources.Models["model_1"].Permissions, 2)
|
||||
require.Contains(t, b.Config.Resources.Models["model_1"].Permissions, resources.Permission{Level: "CAN_MANAGE", UserName: "TestUser"})
|
||||
require.Contains(t, b.Config.Resources.Models["model_1"].Permissions, resources.Permission{Level: "CAN_READ", GroupName: "TestGroup"})
|
||||
|
||||
require.Len(t, b.Config.Resources.Models["model_2"].Permissions, 2)
|
||||
require.Contains(t, b.Config.Resources.Models["model_2"].Permissions, resources.Permission{Level: "CAN_MANAGE", UserName: "TestUser"})
|
||||
require.Contains(t, b.Config.Resources.Models["model_2"].Permissions, resources.Permission{Level: "CAN_READ", GroupName: "TestGroup"})
|
||||
|
||||
require.Len(t, b.Config.Resources.Experiments["experiment_1"].Permissions, 2)
|
||||
require.Contains(t, b.Config.Resources.Experiments["experiment_1"].Permissions, resources.Permission{Level: "CAN_MANAGE", UserName: "TestUser"})
|
||||
require.Contains(t, b.Config.Resources.Experiments["experiment_1"].Permissions, resources.Permission{Level: "CAN_READ", GroupName: "TestGroup"})
|
||||
|
||||
require.Len(t, b.Config.Resources.Experiments["experiment_2"].Permissions, 2)
|
||||
require.Contains(t, b.Config.Resources.Experiments["experiment_2"].Permissions, resources.Permission{Level: "CAN_MANAGE", UserName: "TestUser"})
|
||||
require.Contains(t, b.Config.Resources.Experiments["experiment_2"].Permissions, resources.Permission{Level: "CAN_READ", GroupName: "TestGroup"})
|
||||
|
||||
require.Len(t, b.Config.Resources.ModelServingEndpoints["endpoint_1"].Permissions, 3)
|
||||
require.Contains(t, b.Config.Resources.ModelServingEndpoints["endpoint_1"].Permissions, resources.Permission{Level: "CAN_MANAGE", UserName: "TestUser"})
|
||||
require.Contains(t, b.Config.Resources.ModelServingEndpoints["endpoint_1"].Permissions, resources.Permission{Level: "CAN_VIEW", GroupName: "TestGroup"})
|
||||
require.Contains(t, b.Config.Resources.ModelServingEndpoints["endpoint_1"].Permissions, resources.Permission{Level: "CAN_QUERY", ServicePrincipalName: "TestServicePrincipal"})
|
||||
|
||||
require.Len(t, b.Config.Resources.ModelServingEndpoints["endpoint_2"].Permissions, 3)
|
||||
require.Contains(t, b.Config.Resources.ModelServingEndpoints["endpoint_2"].Permissions, resources.Permission{Level: "CAN_MANAGE", UserName: "TestUser"})
|
||||
require.Contains(t, b.Config.Resources.ModelServingEndpoints["endpoint_2"].Permissions, resources.Permission{Level: "CAN_VIEW", GroupName: "TestGroup"})
|
||||
require.Contains(t, b.Config.Resources.ModelServingEndpoints["endpoint_2"].Permissions, resources.Permission{Level: "CAN_QUERY", ServicePrincipalName: "TestServicePrincipal"})
|
||||
}
|
||||
|
||||
func TestWarningOnOverlapPermission(t *testing.T) {
|
||||
b := &bundle.Bundle{
|
||||
Config: config.Root{
|
||||
Workspace: config.Workspace{
|
||||
RootPath: "/Users/foo@bar.com",
|
||||
},
|
||||
Permissions: []resources.Permission{
|
||||
{Level: CAN_MANAGE, UserName: "TestUser"},
|
||||
{Level: CAN_VIEW, GroupName: "TestGroup"},
|
||||
},
|
||||
Resources: config.Resources{
|
||||
Jobs: map[string]*resources.Job{
|
||||
"job_1": {
|
||||
Permissions: []resources.Permission{
|
||||
{Level: CAN_VIEW, UserName: "TestUser"},
|
||||
},
|
||||
JobSettings: &jobs.JobSettings{},
|
||||
},
|
||||
"job_2": {
|
||||
Permissions: []resources.Permission{
|
||||
{Level: CAN_VIEW, UserName: "TestUser2"},
|
||||
},
|
||||
JobSettings: &jobs.JobSettings{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := bundle.Apply(context.Background(), b, ApplyBundlePermissions())
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Contains(t, b.Config.Resources.Jobs["job_1"].Permissions, resources.Permission{Level: "CAN_VIEW", UserName: "TestUser"})
|
||||
require.Contains(t, b.Config.Resources.Jobs["job_1"].Permissions, resources.Permission{Level: "CAN_VIEW", GroupName: "TestGroup"})
|
||||
require.Contains(t, b.Config.Resources.Jobs["job_2"].Permissions, resources.Permission{Level: "CAN_VIEW", UserName: "TestUser2"})
|
||||
require.Contains(t, b.Config.Resources.Jobs["job_2"].Permissions, resources.Permission{Level: "CAN_MANAGE", UserName: "TestUser"})
|
||||
require.Contains(t, b.Config.Resources.Jobs["job_2"].Permissions, resources.Permission{Level: "CAN_VIEW", GroupName: "TestGroup"})
|
||||
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
package permissions
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/databricks/cli/bundle/config/resources"
|
||||
"github.com/databricks/cli/libs/diag"
|
||||
)
|
||||
|
||||
func convert(
|
||||
ctx context.Context,
|
||||
bundlePermissions []resources.Permission,
|
||||
resourcePermissions []resources.Permission,
|
||||
resourceName string,
|
||||
lm map[string]string,
|
||||
) []resources.Permission {
|
||||
permissions := make([]resources.Permission, 0)
|
||||
for _, p := range bundlePermissions {
|
||||
level, ok := lm[p.Level]
|
||||
// If there is no bundle permission level defined in the map, it means
|
||||
// it's not applicable for the resource, therefore skipping
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if notifyForPermissionOverlap(ctx, p, resourcePermissions, resourceName) {
|
||||
continue
|
||||
}
|
||||
|
||||
permissions = append(permissions, resources.Permission{
|
||||
Level: level,
|
||||
UserName: p.UserName,
|
||||
GroupName: p.GroupName,
|
||||
ServicePrincipalName: p.ServicePrincipalName,
|
||||
})
|
||||
}
|
||||
|
||||
return permissions
|
||||
}
|
||||
|
||||
func isPermissionOverlap(
|
||||
permission resources.Permission,
|
||||
resourcePermissions []resources.Permission,
|
||||
resourceName string,
|
||||
) (bool, diag.Diagnostics) {
|
||||
var diagnostics diag.Diagnostics
|
||||
for _, rp := range resourcePermissions {
|
||||
if rp.GroupName != "" && rp.GroupName == permission.GroupName {
|
||||
diagnostics = diagnostics.Extend(
|
||||
diag.Warningf("'%s' already has permissions set for '%s' group", resourceName, rp.GroupName),
|
||||
)
|
||||
}
|
||||
|
||||
if rp.UserName != "" && rp.UserName == permission.UserName {
|
||||
diagnostics = diagnostics.Extend(
|
||||
diag.Warningf("'%s' already has permissions set for '%s' user name", resourceName, rp.UserName),
|
||||
)
|
||||
}
|
||||
|
||||
if rp.ServicePrincipalName != "" && rp.ServicePrincipalName == permission.ServicePrincipalName {
|
||||
diagnostics = diagnostics.Extend(
|
||||
diag.Warningf("'%s' already has permissions set for '%s' service principal name", resourceName, rp.ServicePrincipalName),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return len(diagnostics) > 0, diagnostics
|
||||
}
|
||||
|
||||
func notifyForPermissionOverlap(
|
||||
ctx context.Context,
|
||||
permission resources.Permission,
|
||||
resourcePermissions []resources.Permission,
|
||||
resourceName string,
|
||||
) bool {
|
||||
isOverlap, _ := isPermissionOverlap(permission, resourcePermissions, resourceName)
|
||||
// TODO: When we start to collect all diagnostics at the top level and visualize jointly,
|
||||
// use diagnostics returned from isPermissionOverlap to display warnings
|
||||
|
||||
return isOverlap
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
package permissions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/databricks/cli/bundle"
|
||||
"github.com/databricks/databricks-sdk-go/service/workspace"
|
||||
)
|
||||
|
||||
type workspaceRootPermissions struct {
|
||||
}
|
||||
|
||||
func ApplyWorkspaceRootPermissions() bundle.Mutator {
|
||||
return &workspaceRootPermissions{}
|
||||
}
|
||||
|
||||
// Apply implements bundle.Mutator.
|
||||
func (*workspaceRootPermissions) Apply(ctx context.Context, b *bundle.Bundle) error {
|
||||
err := giveAccessForWorkspaceRoot(ctx, b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*workspaceRootPermissions) Name() string {
|
||||
return "ApplyWorkspaceRootPermissions"
|
||||
}
|
||||
|
||||
func giveAccessForWorkspaceRoot(ctx context.Context, b *bundle.Bundle) error {
|
||||
permissions := make([]workspace.WorkspaceObjectAccessControlRequest, 0)
|
||||
|
||||
for _, p := range b.Config.Permissions {
|
||||
level, err := getWorkspaceObjectPermissionLevel(p.Level)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
permissions = append(permissions, workspace.WorkspaceObjectAccessControlRequest{
|
||||
GroupName: p.GroupName,
|
||||
UserName: p.UserName,
|
||||
ServicePrincipalName: p.ServicePrincipalName,
|
||||
PermissionLevel: level,
|
||||
})
|
||||
}
|
||||
|
||||
if len(permissions) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
w := b.WorkspaceClient().Workspace
|
||||
obj, err := w.GetStatusByPath(ctx, b.Config.Workspace.RootPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = w.UpdatePermissions(ctx, workspace.WorkspaceObjectPermissionsRequest{
|
||||
WorkspaceObjectId: fmt.Sprint(obj.ObjectId),
|
||||
WorkspaceObjectType: "directories",
|
||||
AccessControlList: permissions,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func getWorkspaceObjectPermissionLevel(bundlePermission string) (workspace.WorkspaceObjectPermissionLevel, error) {
|
||||
switch bundlePermission {
|
||||
case CAN_MANAGE:
|
||||
return workspace.WorkspaceObjectPermissionLevelCanManage, nil
|
||||
case CAN_RUN:
|
||||
return workspace.WorkspaceObjectPermissionLevelCanRun, nil
|
||||
case CAN_VIEW:
|
||||
return workspace.WorkspaceObjectPermissionLevelCanRead, nil
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported bundle permission level %s", bundlePermission)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
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/jobs"
|
||||
"github.com/databricks/databricks-sdk-go/service/ml"
|
||||
"github.com/databricks/databricks-sdk-go/service/pipelines"
|
||||
"github.com/databricks/databricks-sdk-go/service/serving"
|
||||
"github.com/databricks/databricks-sdk-go/service/workspace"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type MockWorkspaceClient struct {
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
// Delete implements workspace.WorkspaceService.
|
||||
func (MockWorkspaceClient) Delete(ctx context.Context, request workspace.Delete) error {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// Export implements workspace.WorkspaceService.
|
||||
func (MockWorkspaceClient) Export(ctx context.Context, request workspace.ExportRequest) (*workspace.ExportResponse, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// GetPermissionLevels implements workspace.WorkspaceService.
|
||||
func (MockWorkspaceClient) GetPermissionLevels(ctx context.Context, request workspace.GetWorkspaceObjectPermissionLevelsRequest) (*workspace.GetWorkspaceObjectPermissionLevelsResponse, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// GetPermissions implements workspace.WorkspaceService.
|
||||
func (MockWorkspaceClient) GetPermissions(ctx context.Context, request workspace.GetWorkspaceObjectPermissionsRequest) (*workspace.WorkspaceObjectPermissions, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// GetStatus implements workspace.WorkspaceService.
|
||||
func (MockWorkspaceClient) GetStatus(ctx context.Context, request workspace.GetStatusRequest) (*workspace.ObjectInfo, error) {
|
||||
return &workspace.ObjectInfo{
|
||||
ObjectId: 1234, ObjectType: "directories", Path: "/Users/foo@bar.com",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Import implements workspace.WorkspaceService.
|
||||
func (MockWorkspaceClient) Import(ctx context.Context, request workspace.Import) error {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// List implements workspace.WorkspaceService.
|
||||
func (MockWorkspaceClient) List(ctx context.Context, request workspace.ListWorkspaceRequest) (*workspace.ListResponse, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// Mkdirs implements workspace.WorkspaceService.
|
||||
func (MockWorkspaceClient) Mkdirs(ctx context.Context, request workspace.Mkdirs) error {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// SetPermissions implements workspace.WorkspaceService.
|
||||
func (MockWorkspaceClient) SetPermissions(ctx context.Context, request workspace.WorkspaceObjectPermissionsRequest) (*workspace.WorkspaceObjectPermissions, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// UpdatePermissions implements workspace.WorkspaceService.
|
||||
func (m MockWorkspaceClient) UpdatePermissions(ctx context.Context, request workspace.WorkspaceObjectPermissionsRequest) (*workspace.WorkspaceObjectPermissions, error) {
|
||||
require.Equal(m.t, "1234", request.WorkspaceObjectId)
|
||||
require.Equal(m.t, "directories", request.WorkspaceObjectType)
|
||||
require.Contains(m.t, request.AccessControlList, workspace.WorkspaceObjectAccessControlRequest{
|
||||
UserName: "TestUser",
|
||||
PermissionLevel: "CAN_MANAGE",
|
||||
})
|
||||
require.Contains(m.t, request.AccessControlList, workspace.WorkspaceObjectAccessControlRequest{
|
||||
GroupName: "TestGroup",
|
||||
PermissionLevel: "CAN_READ",
|
||||
})
|
||||
require.Contains(m.t, request.AccessControlList, workspace.WorkspaceObjectAccessControlRequest{
|
||||
ServicePrincipalName: "TestServicePrincipal",
|
||||
PermissionLevel: "CAN_RUN",
|
||||
})
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func TestApplyWorkspaceRootPermissions(t *testing.T) {
|
||||
b := &bundle.Bundle{
|
||||
Config: config.Root{
|
||||
Workspace: config.Workspace{
|
||||
RootPath: "/Users/foo@bar.com",
|
||||
},
|
||||
Permissions: []resources.Permission{
|
||||
{Level: CAN_MANAGE, UserName: "TestUser"},
|
||||
{Level: CAN_VIEW, GroupName: "TestGroup"},
|
||||
{Level: CAN_RUN, ServicePrincipalName: "TestServicePrincipal"},
|
||||
},
|
||||
Resources: config.Resources{
|
||||
Jobs: map[string]*resources.Job{
|
||||
"job_1": {JobSettings: &jobs.JobSettings{}},
|
||||
"job_2": {JobSettings: &jobs.JobSettings{}},
|
||||
},
|
||||
Pipelines: map[string]*resources.Pipeline{
|
||||
"pipeline_1": {PipelineSpec: &pipelines.PipelineSpec{}},
|
||||
"pipeline_2": {PipelineSpec: &pipelines.PipelineSpec{}},
|
||||
},
|
||||
Models: map[string]*resources.MlflowModel{
|
||||
"model_1": {Model: &ml.Model{}},
|
||||
"model_2": {Model: &ml.Model{}},
|
||||
},
|
||||
Experiments: map[string]*resources.MlflowExperiment{
|
||||
"experiment_1": {Experiment: &ml.Experiment{}},
|
||||
"experiment_2": {Experiment: &ml.Experiment{}},
|
||||
},
|
||||
ModelServingEndpoints: map[string]*resources.ModelServingEndpoint{
|
||||
"endpoint_1": {CreateServingEndpoint: &serving.CreateServingEndpoint{}},
|
||||
"endpoint_2": {CreateServingEndpoint: &serving.CreateServingEndpoint{}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
b.WorkspaceClient().Workspace.WithImpl(MockWorkspaceClient{t})
|
||||
|
||||
err := bundle.Apply(context.Background(), b, ApplyWorkspaceRootPermissions())
|
||||
require.NoError(t, err)
|
||||
}
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/databricks/cli/bundle/deploy/metadata"
|
||||
"github.com/databricks/cli/bundle/deploy/terraform"
|
||||
"github.com/databricks/cli/bundle/libraries"
|
||||
"github.com/databricks/cli/bundle/permissions"
|
||||
"github.com/databricks/cli/bundle/python"
|
||||
"github.com/databricks/cli/bundle/scripts"
|
||||
)
|
||||
|
@ -27,6 +28,7 @@ func Deploy() bundle.Mutator {
|
|||
artifacts.UploadAll(),
|
||||
python.TransformWheelTask(),
|
||||
files.Upload(),
|
||||
permissions.ApplyWorkspaceRootPermissions(),
|
||||
terraform.Interpolate(),
|
||||
terraform.Write(),
|
||||
terraform.StatePull(),
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/databricks/cli/bundle/config/mutator"
|
||||
"github.com/databricks/cli/bundle/config/variable"
|
||||
"github.com/databricks/cli/bundle/deploy/terraform"
|
||||
"github.com/databricks/cli/bundle/permissions"
|
||||
"github.com/databricks/cli/bundle/python"
|
||||
"github.com/databricks/cli/bundle/scripts"
|
||||
)
|
||||
|
@ -34,6 +35,7 @@ func Initialize() bundle.Mutator {
|
|||
mutator.ExpandPipelineGlobPaths(),
|
||||
mutator.TranslatePaths(),
|
||||
python.WrapperWarning(),
|
||||
permissions.ApplyBundlePermissions(),
|
||||
terraform.Initialize(),
|
||||
scripts.Execute(config.ScriptPostInit),
|
||||
},
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
bundle:
|
||||
name: bundle_permissions
|
||||
|
||||
permissions:
|
||||
- level: CAN_RUN
|
||||
user_name: test@company.com
|
||||
|
||||
targets:
|
||||
development:
|
||||
permissions:
|
||||
- level: CAN_MANAGE
|
||||
group_name: devs
|
||||
- level: CAN_VIEW
|
||||
service_principal_name: 1234-abcd
|
||||
- level: CAN_RUN
|
||||
user_name: bot@company.com
|
||||
|
||||
resources:
|
||||
pipelines:
|
||||
nyc_taxi_pipeline:
|
||||
target: nyc_taxi_production
|
||||
development: false
|
||||
photon: true
|
||||
|
||||
jobs:
|
||||
pipeline_schedule:
|
||||
name: Daily refresh of production pipeline
|
||||
|
||||
schedule:
|
||||
quartz_cron_expression: 6 6 11 * * ?
|
||||
timezone_id: UTC
|
||||
|
||||
tasks:
|
||||
- pipeline_task:
|
||||
pipeline_id: "to be interpolated"
|
|
@ -0,0 +1,56 @@
|
|||
package config_tests
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/databricks/cli/bundle"
|
||||
"github.com/databricks/cli/bundle/config/resources"
|
||||
"github.com/databricks/cli/bundle/permissions"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBundlePermissions(t *testing.T) {
|
||||
b := load(t, "./bundle_permissions")
|
||||
assert.Contains(t, b.Config.Permissions, resources.Permission{Level: "CAN_RUN", UserName: "test@company.com"})
|
||||
assert.NotContains(t, b.Config.Permissions, resources.Permission{Level: "CAN_MANAGE", GroupName: "devs"})
|
||||
assert.NotContains(t, b.Config.Permissions, resources.Permission{Level: "CAN_VIEW", ServicePrincipalName: "1234-abcd"})
|
||||
assert.NotContains(t, b.Config.Permissions, resources.Permission{Level: "CAN_RUN", UserName: "bot@company.com"})
|
||||
|
||||
err := bundle.Apply(context.Background(), b, permissions.ApplyBundlePermissions())
|
||||
require.NoError(t, err)
|
||||
pipelinePermissions := b.Config.Resources.Pipelines["nyc_taxi_pipeline"].Permissions
|
||||
assert.Contains(t, pipelinePermissions, resources.Permission{Level: "CAN_RUN", UserName: "test@company.com"})
|
||||
assert.NotContains(t, pipelinePermissions, resources.Permission{Level: "CAN_MANAGE", GroupName: "devs"})
|
||||
assert.NotContains(t, pipelinePermissions, resources.Permission{Level: "CAN_VIEW", ServicePrincipalName: "1234-abcd"})
|
||||
assert.NotContains(t, pipelinePermissions, resources.Permission{Level: "CAN_RUN", UserName: "bot@company.com"})
|
||||
|
||||
jobsPermissions := b.Config.Resources.Jobs["pipeline_schedule"].Permissions
|
||||
assert.Contains(t, jobsPermissions, resources.Permission{Level: "CAN_MANAGE_RUN", UserName: "test@company.com"})
|
||||
assert.NotContains(t, jobsPermissions, resources.Permission{Level: "CAN_MANAGE", GroupName: "devs"})
|
||||
assert.NotContains(t, jobsPermissions, resources.Permission{Level: "CAN_VIEW", ServicePrincipalName: "1234-abcd"})
|
||||
assert.NotContains(t, jobsPermissions, resources.Permission{Level: "CAN_RUN", UserName: "bot@company.com"})
|
||||
}
|
||||
|
||||
func TestBundlePermissionsDevTarget(t *testing.T) {
|
||||
b := loadTarget(t, "./bundle_permissions", "development")
|
||||
assert.Contains(t, b.Config.Permissions, resources.Permission{Level: "CAN_RUN", UserName: "test@company.com"})
|
||||
assert.Contains(t, b.Config.Permissions, resources.Permission{Level: "CAN_MANAGE", GroupName: "devs"})
|
||||
assert.Contains(t, b.Config.Permissions, resources.Permission{Level: "CAN_VIEW", ServicePrincipalName: "1234-abcd"})
|
||||
assert.Contains(t, b.Config.Permissions, resources.Permission{Level: "CAN_RUN", UserName: "bot@company.com"})
|
||||
|
||||
err := bundle.Apply(context.Background(), b, permissions.ApplyBundlePermissions())
|
||||
require.NoError(t, err)
|
||||
pipelinePermissions := b.Config.Resources.Pipelines["nyc_taxi_pipeline"].Permissions
|
||||
assert.Contains(t, pipelinePermissions, resources.Permission{Level: "CAN_RUN", UserName: "test@company.com"})
|
||||
assert.Contains(t, pipelinePermissions, resources.Permission{Level: "CAN_MANAGE", GroupName: "devs"})
|
||||
assert.Contains(t, pipelinePermissions, resources.Permission{Level: "CAN_VIEW", ServicePrincipalName: "1234-abcd"})
|
||||
assert.Contains(t, pipelinePermissions, resources.Permission{Level: "CAN_RUN", UserName: "bot@company.com"})
|
||||
|
||||
jobsPermissions := b.Config.Resources.Jobs["pipeline_schedule"].Permissions
|
||||
assert.Contains(t, jobsPermissions, resources.Permission{Level: "CAN_MANAGE_RUN", UserName: "test@company.com"})
|
||||
assert.Contains(t, jobsPermissions, resources.Permission{Level: "CAN_MANAGE", GroupName: "devs"})
|
||||
assert.Contains(t, jobsPermissions, resources.Permission{Level: "CAN_VIEW", ServicePrincipalName: "1234-abcd"})
|
||||
assert.Contains(t, jobsPermissions, resources.Permission{Level: "CAN_MANAGE_RUN", UserName: "bot@company.com"})
|
||||
}
|
Loading…
Reference in New Issue