mirror of https://github.com/databricks/cli.git
Added validator for folder permissions (#1824)
## Changes This validator checks permissions defined in top-level bundle config and permissions set in workspace for the folders bundle is deployed to. It raises the warning if the permissions defined in the workspace are not defined in bundle. This validator is executed only during `bundle validate` command. ## Tests ``` Warning: untracked permissions apply to target workspace path The following permissions apply to the workspace folder at "/Workspace/Users/andrew.nester@databricks.com/.bundle/clusters/default" but are not configured in the bundle: - level: CAN_MANAGE, user_name: andrew.nester@databricks.com ``` --------- Co-authored-by: Pieter Noordhuis <pieter.noordhuis@databricks.com>
This commit is contained in:
parent
89ee7d8a99
commit
eaea308254
|
@ -1,5 +1,7 @@
|
||||||
package resources
|
package resources
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
// Permission holds the permission level setting for a single principal.
|
// Permission holds the permission level setting for a single principal.
|
||||||
// Multiple of these can be defined on any resource.
|
// Multiple of these can be defined on any resource.
|
||||||
type Permission struct {
|
type Permission struct {
|
||||||
|
@ -9,3 +11,19 @@ type Permission struct {
|
||||||
ServicePrincipalName string `json:"service_principal_name,omitempty"`
|
ServicePrincipalName string `json:"service_principal_name,omitempty"`
|
||||||
GroupName string `json:"group_name,omitempty"`
|
GroupName string `json:"group_name,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p Permission) String() string {
|
||||||
|
if p.UserName != "" {
|
||||||
|
return fmt.Sprintf("level: %s, user_name: %s", p.Level, p.UserName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.ServicePrincipalName != "" {
|
||||||
|
return fmt.Sprintf("level: %s, service_principal_name: %s", p.Level, p.ServicePrincipalName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.GroupName != "" {
|
||||||
|
return fmt.Sprintf("level: %s, group_name: %s", p.Level, p.GroupName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("level: %s", p.Level)
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,126 @@
|
||||||
|
package validate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/databricks/cli/bundle"
|
||||||
|
"github.com/databricks/cli/bundle/libraries"
|
||||||
|
"github.com/databricks/cli/bundle/permissions"
|
||||||
|
"github.com/databricks/cli/libs/diag"
|
||||||
|
"github.com/databricks/databricks-sdk-go/apierr"
|
||||||
|
"github.com/databricks/databricks-sdk-go/service/workspace"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
)
|
||||||
|
|
||||||
|
type folderPermissions struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply implements bundle.ReadOnlyMutator.
|
||||||
|
func (f *folderPermissions) Apply(ctx context.Context, b bundle.ReadOnlyBundle) diag.Diagnostics {
|
||||||
|
if len(b.Config().Permissions) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rootPath := b.Config().Workspace.RootPath
|
||||||
|
paths := []string{}
|
||||||
|
if !libraries.IsVolumesPath(rootPath) && !libraries.IsWorkspaceSharedPath(rootPath) {
|
||||||
|
paths = append(paths, rootPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasSuffix(rootPath, "/") {
|
||||||
|
rootPath += "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range []string{
|
||||||
|
b.Config().Workspace.ArtifactPath,
|
||||||
|
b.Config().Workspace.FilePath,
|
||||||
|
b.Config().Workspace.StatePath,
|
||||||
|
b.Config().Workspace.ResourcePath,
|
||||||
|
} {
|
||||||
|
if libraries.IsWorkspaceSharedPath(p) || libraries.IsVolumesPath(p) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(p, rootPath) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
paths = append(paths, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
var diags diag.Diagnostics
|
||||||
|
g, ctx := errgroup.WithContext(ctx)
|
||||||
|
results := make([]diag.Diagnostics, len(paths))
|
||||||
|
for i, p := range paths {
|
||||||
|
g.Go(func() error {
|
||||||
|
results[i] = checkFolderPermission(ctx, b, p)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.Wait(); err != nil {
|
||||||
|
return diag.FromErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range results {
|
||||||
|
diags = diags.Extend(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkFolderPermission(ctx context.Context, b bundle.ReadOnlyBundle, folderPath string) diag.Diagnostics {
|
||||||
|
w := b.WorkspaceClient().Workspace
|
||||||
|
obj, err := getClosestExistingObject(ctx, w, folderPath)
|
||||||
|
if err != nil {
|
||||||
|
return diag.FromErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
objPermissions, err := w.GetPermissions(ctx, workspace.GetWorkspaceObjectPermissionsRequest{
|
||||||
|
WorkspaceObjectId: fmt.Sprint(obj.ObjectId),
|
||||||
|
WorkspaceObjectType: "directories",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return diag.FromErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
p := permissions.ObjectAclToResourcePermissions(folderPath, objPermissions.AccessControlList)
|
||||||
|
return p.Compare(b.Config().Permissions)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getClosestExistingObject(ctx context.Context, w workspace.WorkspaceInterface, folderPath string) (*workspace.ObjectInfo, error) {
|
||||||
|
for {
|
||||||
|
obj, err := w.GetStatusByPath(ctx, folderPath)
|
||||||
|
if err == nil {
|
||||||
|
return obj, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !apierr.IsMissing(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
parent := path.Dir(folderPath)
|
||||||
|
// If the parent is the same as the current folder, then we have reached the root
|
||||||
|
if folderPath == parent {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
folderPath = parent
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("folder %s and its parent folders do not exist", folderPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name implements bundle.ReadOnlyMutator.
|
||||||
|
func (f *folderPermissions) Name() string {
|
||||||
|
return "validate:folder_permissions"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateFolderPermissions validates that permissions for the folders in Workspace file system matches
|
||||||
|
// the permissions in the top-level permissions section of the bundle.
|
||||||
|
func ValidateFolderPermissions() bundle.ReadOnlyMutator {
|
||||||
|
return &folderPermissions{}
|
||||||
|
}
|
|
@ -0,0 +1,208 @@
|
||||||
|
package validate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/databricks/cli/bundle"
|
||||||
|
"github.com/databricks/cli/bundle/config"
|
||||||
|
"github.com/databricks/cli/bundle/config/resources"
|
||||||
|
"github.com/databricks/cli/bundle/permissions"
|
||||||
|
"github.com/databricks/cli/libs/diag"
|
||||||
|
"github.com/databricks/databricks-sdk-go/apierr"
|
||||||
|
"github.com/databricks/databricks-sdk-go/experimental/mocks"
|
||||||
|
"github.com/databricks/databricks-sdk-go/service/workspace"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFolderPermissionsInheritedWhenRootPathDoesNotExist(t *testing.T) {
|
||||||
|
b := &bundle.Bundle{
|
||||||
|
Config: config.Root{
|
||||||
|
Workspace: config.Workspace{
|
||||||
|
RootPath: "/Workspace/Users/foo@bar.com",
|
||||||
|
ArtifactPath: "/Workspace/Users/otherfoo@bar.com/artifacts",
|
||||||
|
FilePath: "/Workspace/Users/foo@bar.com/files",
|
||||||
|
StatePath: "/Workspace/Users/foo@bar.com/state",
|
||||||
|
ResourcePath: "/Workspace/Users/foo@bar.com/resources",
|
||||||
|
},
|
||||||
|
Permissions: []resources.Permission{
|
||||||
|
{Level: permissions.CAN_MANAGE, UserName: "foo@bar.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
m := mocks.NewMockWorkspaceClient(t)
|
||||||
|
api := m.GetMockWorkspaceAPI()
|
||||||
|
api.EXPECT().GetStatusByPath(mock.Anything, "/Workspace/Users/otherfoo@bar.com/artifacts").Return(nil, &apierr.APIError{
|
||||||
|
StatusCode: 404,
|
||||||
|
ErrorCode: "RESOURCE_DOES_NOT_EXIST",
|
||||||
|
})
|
||||||
|
api.EXPECT().GetStatusByPath(mock.Anything, "/Workspace/Users/otherfoo@bar.com").Return(nil, &apierr.APIError{
|
||||||
|
StatusCode: 404,
|
||||||
|
ErrorCode: "RESOURCE_DOES_NOT_EXIST",
|
||||||
|
})
|
||||||
|
api.EXPECT().GetStatusByPath(mock.Anything, "/Workspace/Users/foo@bar.com").Return(nil, &apierr.APIError{
|
||||||
|
StatusCode: 404,
|
||||||
|
ErrorCode: "RESOURCE_DOES_NOT_EXIST",
|
||||||
|
})
|
||||||
|
api.EXPECT().GetStatusByPath(mock.Anything, "/Workspace/Users").Return(nil, &apierr.APIError{
|
||||||
|
StatusCode: 404,
|
||||||
|
ErrorCode: "RESOURCE_DOES_NOT_EXIST",
|
||||||
|
})
|
||||||
|
api.EXPECT().GetStatusByPath(mock.Anything, "/Workspace").Return(&workspace.ObjectInfo{
|
||||||
|
ObjectId: 1234,
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
api.EXPECT().GetPermissions(mock.Anything, workspace.GetWorkspaceObjectPermissionsRequest{
|
||||||
|
WorkspaceObjectId: "1234",
|
||||||
|
WorkspaceObjectType: "directories",
|
||||||
|
}).Return(&workspace.WorkspaceObjectPermissions{
|
||||||
|
ObjectId: "1234",
|
||||||
|
AccessControlList: []workspace.WorkspaceObjectAccessControlResponse{
|
||||||
|
{
|
||||||
|
UserName: "foo@bar.com",
|
||||||
|
AllPermissions: []workspace.WorkspaceObjectPermission{
|
||||||
|
{PermissionLevel: "CAN_MANAGE"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
b.SetWorkpaceClient(m.WorkspaceClient)
|
||||||
|
rb := bundle.ReadOnly(b)
|
||||||
|
|
||||||
|
diags := bundle.ApplyReadOnly(context.Background(), rb, ValidateFolderPermissions())
|
||||||
|
require.Empty(t, diags)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateFolderPermissionsFailsOnMissingBundlePermission(t *testing.T) {
|
||||||
|
b := &bundle.Bundle{
|
||||||
|
Config: config.Root{
|
||||||
|
Workspace: config.Workspace{
|
||||||
|
RootPath: "/Workspace/Users/foo@bar.com",
|
||||||
|
ArtifactPath: "/Workspace/Users/foo@bar.com/artifacts",
|
||||||
|
FilePath: "/Workspace/Users/foo@bar.com/files",
|
||||||
|
StatePath: "/Workspace/Users/foo@bar.com/state",
|
||||||
|
ResourcePath: "/Workspace/Users/foo@bar.com/resources",
|
||||||
|
},
|
||||||
|
Permissions: []resources.Permission{
|
||||||
|
{Level: permissions.CAN_MANAGE, UserName: "foo@bar.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
m := mocks.NewMockWorkspaceClient(t)
|
||||||
|
api := m.GetMockWorkspaceAPI()
|
||||||
|
api.EXPECT().GetStatusByPath(mock.Anything, "/Workspace/Users/foo@bar.com").Return(&workspace.ObjectInfo{
|
||||||
|
ObjectId: 1234,
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
api.EXPECT().GetPermissions(mock.Anything, workspace.GetWorkspaceObjectPermissionsRequest{
|
||||||
|
WorkspaceObjectId: "1234",
|
||||||
|
WorkspaceObjectType: "directories",
|
||||||
|
}).Return(&workspace.WorkspaceObjectPermissions{
|
||||||
|
ObjectId: "1234",
|
||||||
|
AccessControlList: []workspace.WorkspaceObjectAccessControlResponse{
|
||||||
|
{
|
||||||
|
UserName: "foo@bar.com",
|
||||||
|
AllPermissions: []workspace.WorkspaceObjectPermission{
|
||||||
|
{PermissionLevel: "CAN_MANAGE"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
UserName: "foo2@bar.com",
|
||||||
|
AllPermissions: []workspace.WorkspaceObjectPermission{
|
||||||
|
{PermissionLevel: "CAN_MANAGE"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
b.SetWorkpaceClient(m.WorkspaceClient)
|
||||||
|
rb := bundle.ReadOnly(b)
|
||||||
|
|
||||||
|
diags := bundle.ApplyReadOnly(context.Background(), rb, ValidateFolderPermissions())
|
||||||
|
require.Len(t, diags, 1)
|
||||||
|
require.Equal(t, "untracked permissions apply to target workspace path", diags[0].Summary)
|
||||||
|
require.Equal(t, diag.Warning, diags[0].Severity)
|
||||||
|
require.Equal(t, "The following permissions apply to the workspace folder at \"/Workspace/Users/foo@bar.com\" but are not configured in the bundle:\n- level: CAN_MANAGE, user_name: foo2@bar.com\n", diags[0].Detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateFolderPermissionsFailsOnPermissionMismatch(t *testing.T) {
|
||||||
|
b := &bundle.Bundle{
|
||||||
|
Config: config.Root{
|
||||||
|
Workspace: config.Workspace{
|
||||||
|
RootPath: "/Workspace/Users/foo@bar.com",
|
||||||
|
ArtifactPath: "/Workspace/Users/foo@bar.com/artifacts",
|
||||||
|
FilePath: "/Workspace/Users/foo@bar.com/files",
|
||||||
|
StatePath: "/Workspace/Users/foo@bar.com/state",
|
||||||
|
ResourcePath: "/Workspace/Users/foo@bar.com/resources",
|
||||||
|
},
|
||||||
|
Permissions: []resources.Permission{
|
||||||
|
{Level: permissions.CAN_MANAGE, UserName: "foo@bar.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
m := mocks.NewMockWorkspaceClient(t)
|
||||||
|
api := m.GetMockWorkspaceAPI()
|
||||||
|
api.EXPECT().GetStatusByPath(mock.Anything, "/Workspace/Users/foo@bar.com").Return(&workspace.ObjectInfo{
|
||||||
|
ObjectId: 1234,
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
api.EXPECT().GetPermissions(mock.Anything, workspace.GetWorkspaceObjectPermissionsRequest{
|
||||||
|
WorkspaceObjectId: "1234",
|
||||||
|
WorkspaceObjectType: "directories",
|
||||||
|
}).Return(&workspace.WorkspaceObjectPermissions{
|
||||||
|
ObjectId: "1234",
|
||||||
|
AccessControlList: []workspace.WorkspaceObjectAccessControlResponse{
|
||||||
|
{
|
||||||
|
UserName: "foo2@bar.com",
|
||||||
|
AllPermissions: []workspace.WorkspaceObjectPermission{
|
||||||
|
{PermissionLevel: "CAN_MANAGE"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
b.SetWorkpaceClient(m.WorkspaceClient)
|
||||||
|
rb := bundle.ReadOnly(b)
|
||||||
|
|
||||||
|
diags := bundle.ApplyReadOnly(context.Background(), rb, ValidateFolderPermissions())
|
||||||
|
require.Len(t, diags, 1)
|
||||||
|
require.Equal(t, "untracked permissions apply to target workspace path", diags[0].Summary)
|
||||||
|
require.Equal(t, diag.Warning, diags[0].Severity)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateFolderPermissionsFailsOnNoRootFolder(t *testing.T) {
|
||||||
|
b := &bundle.Bundle{
|
||||||
|
Config: config.Root{
|
||||||
|
Workspace: config.Workspace{
|
||||||
|
RootPath: "/NotExisting",
|
||||||
|
ArtifactPath: "/NotExisting/artifacts",
|
||||||
|
FilePath: "/NotExisting/files",
|
||||||
|
StatePath: "/NotExisting/state",
|
||||||
|
ResourcePath: "/NotExisting/resources",
|
||||||
|
},
|
||||||
|
Permissions: []resources.Permission{
|
||||||
|
{Level: permissions.CAN_MANAGE, UserName: "foo@bar.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
m := mocks.NewMockWorkspaceClient(t)
|
||||||
|
api := m.GetMockWorkspaceAPI()
|
||||||
|
api.EXPECT().GetStatusByPath(mock.Anything, "/NotExisting").Return(nil, &apierr.APIError{
|
||||||
|
StatusCode: 404,
|
||||||
|
ErrorCode: "RESOURCE_DOES_NOT_EXIST",
|
||||||
|
})
|
||||||
|
api.EXPECT().GetStatusByPath(mock.Anything, "/").Return(nil, &apierr.APIError{
|
||||||
|
StatusCode: 404,
|
||||||
|
ErrorCode: "RESOURCE_DOES_NOT_EXIST",
|
||||||
|
})
|
||||||
|
|
||||||
|
b.SetWorkpaceClient(m.WorkspaceClient)
|
||||||
|
rb := bundle.ReadOnly(b)
|
||||||
|
|
||||||
|
diags := bundle.ApplyReadOnly(context.Background(), rb, ValidateFolderPermissions())
|
||||||
|
require.Len(t, diags, 1)
|
||||||
|
require.Equal(t, "folder / and its parent folders do not exist", diags[0].Summary)
|
||||||
|
require.Equal(t, diag.Error, diags[0].Severity)
|
||||||
|
}
|
|
@ -35,6 +35,7 @@ func (v *validate) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics
|
||||||
FilesToSync(),
|
FilesToSync(),
|
||||||
ValidateSyncPatterns(),
|
ValidateSyncPatterns(),
|
||||||
JobTaskClusterSpec(),
|
JobTaskClusterSpec(),
|
||||||
|
ValidateFolderPermissions(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,3 +36,12 @@ func IsWorkspaceLibrary(library *compute.Library) bool {
|
||||||
|
|
||||||
return IsWorkspacePath(path)
|
return IsWorkspacePath(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsVolumesPath returns true if the specified path indicates that
|
||||||
|
func IsVolumesPath(path string) bool {
|
||||||
|
return strings.HasPrefix(path, "/Volumes/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsWorkspaceSharedPath(path string) bool {
|
||||||
|
return strings.HasPrefix(path, "/Workspace/Shared/")
|
||||||
|
}
|
|
@ -31,3 +31,13 @@ func TestIsWorkspaceLibrary(t *testing.T) {
|
||||||
// Empty.
|
// Empty.
|
||||||
assert.False(t, IsWorkspaceLibrary(&compute.Library{}))
|
assert.False(t, IsWorkspaceLibrary(&compute.Library{}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIsVolumesPath(t *testing.T) {
|
||||||
|
// Absolute paths with particular prefixes.
|
||||||
|
assert.True(t, IsVolumesPath("/Volumes/path/to/package"))
|
||||||
|
|
||||||
|
// Relative paths.
|
||||||
|
assert.False(t, IsVolumesPath("myfile.txt"))
|
||||||
|
assert.False(t, IsVolumesPath("./myfile.txt"))
|
||||||
|
assert.False(t, IsVolumesPath("../myfile.txt"))
|
||||||
|
}
|
|
@ -3,9 +3,9 @@ package permissions
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/databricks/cli/bundle"
|
"github.com/databricks/cli/bundle"
|
||||||
|
"github.com/databricks/cli/bundle/libraries"
|
||||||
"github.com/databricks/cli/libs/diag"
|
"github.com/databricks/cli/libs/diag"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -21,17 +21,13 @@ func (*validateSharedRootPermissions) Name() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*validateSharedRootPermissions) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
|
func (*validateSharedRootPermissions) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
|
||||||
if isWorkspaceSharedRoot(b.Config.Workspace.RootPath) {
|
if libraries.IsWorkspaceSharedPath(b.Config.Workspace.RootPath) {
|
||||||
return isUsersGroupPermissionSet(b)
|
return isUsersGroupPermissionSet(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func isWorkspaceSharedRoot(path string) bool {
|
|
||||||
return strings.HasPrefix(path, "/Workspace/Shared/")
|
|
||||||
}
|
|
||||||
|
|
||||||
// isUsersGroupPermissionSet checks that top-level permissions set for bundle contain group_name: users with CAN_MANAGE permission.
|
// isUsersGroupPermissionSet checks that top-level permissions set for bundle contain group_name: users with CAN_MANAGE permission.
|
||||||
func isUsersGroupPermissionSet(b *bundle.Bundle) diag.Diagnostics {
|
func isUsersGroupPermissionSet(b *bundle.Bundle) diag.Diagnostics {
|
||||||
var diags diag.Diagnostics
|
var diags diag.Diagnostics
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
package permissions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/databricks/cli/bundle/config/resources"
|
||||||
|
"github.com/databricks/cli/libs/diag"
|
||||||
|
"github.com/databricks/databricks-sdk-go/service/workspace"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WorkspacePathPermissions struct {
|
||||||
|
Path string
|
||||||
|
Permissions []resources.Permission
|
||||||
|
}
|
||||||
|
|
||||||
|
func ObjectAclToResourcePermissions(path string, acl []workspace.WorkspaceObjectAccessControlResponse) *WorkspacePathPermissions {
|
||||||
|
permissions := make([]resources.Permission, 0)
|
||||||
|
for _, a := range acl {
|
||||||
|
// Skip the admin group because it's added to all resources by default.
|
||||||
|
if a.GroupName == "admins" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pl := range a.AllPermissions {
|
||||||
|
permissions = append(permissions, resources.Permission{
|
||||||
|
Level: convertWorkspaceObjectPermissionLevel(pl.PermissionLevel),
|
||||||
|
GroupName: a.GroupName,
|
||||||
|
UserName: a.UserName,
|
||||||
|
ServicePrincipalName: a.ServicePrincipalName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &WorkspacePathPermissions{Permissions: permissions, Path: path}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p WorkspacePathPermissions) Compare(perms []resources.Permission) diag.Diagnostics {
|
||||||
|
var diags diag.Diagnostics
|
||||||
|
|
||||||
|
// Check the permissions in the workspace and see if they are all set in the bundle.
|
||||||
|
ok, missing := containsAll(p.Permissions, perms)
|
||||||
|
if !ok {
|
||||||
|
diags = diags.Append(diag.Diagnostic{
|
||||||
|
Severity: diag.Warning,
|
||||||
|
Summary: "untracked permissions apply to target workspace path",
|
||||||
|
Detail: fmt.Sprintf("The following permissions apply to the workspace folder at %q but are not configured in the bundle:\n%s", p.Path, toString(missing)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
|
// containsAll checks if permA contains all permissions in permB.
|
||||||
|
func containsAll(permA []resources.Permission, permB []resources.Permission) (bool, []resources.Permission) {
|
||||||
|
missing := make([]resources.Permission, 0)
|
||||||
|
for _, a := range permA {
|
||||||
|
found := false
|
||||||
|
for _, b := range permB {
|
||||||
|
if a == b {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
missing = append(missing, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return len(missing) == 0, missing
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertWorkspaceObjectPermissionLevel converts matching object permission levels to bundle ones.
|
||||||
|
// If there is no matching permission level, it returns permission level as is, for example, CAN_EDIT.
|
||||||
|
func convertWorkspaceObjectPermissionLevel(level workspace.WorkspaceObjectPermissionLevel) string {
|
||||||
|
switch level {
|
||||||
|
case workspace.WorkspaceObjectPermissionLevelCanRead:
|
||||||
|
return CAN_VIEW
|
||||||
|
default:
|
||||||
|
return string(level)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toString(p []resources.Permission) string {
|
||||||
|
var sb strings.Builder
|
||||||
|
for _, perm := range p {
|
||||||
|
sb.WriteString(fmt.Sprintf("- %s\n", perm.String()))
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
|
@ -0,0 +1,121 @@
|
||||||
|
package permissions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/databricks/cli/bundle/config/resources"
|
||||||
|
"github.com/databricks/cli/libs/diag"
|
||||||
|
"github.com/databricks/databricks-sdk-go/service/workspace"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWorkspacePathPermissionsCompare(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
perms []resources.Permission
|
||||||
|
acl []workspace.WorkspaceObjectAccessControlResponse
|
||||||
|
expected diag.Diagnostics
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
perms: []resources.Permission{
|
||||||
|
{Level: CAN_MANAGE, UserName: "foo@bar.com"},
|
||||||
|
},
|
||||||
|
acl: []workspace.WorkspaceObjectAccessControlResponse{
|
||||||
|
{
|
||||||
|
UserName: "foo@bar.com",
|
||||||
|
AllPermissions: []workspace.WorkspaceObjectPermission{
|
||||||
|
{PermissionLevel: "CAN_MANAGE"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
perms: []resources.Permission{
|
||||||
|
{Level: CAN_MANAGE, UserName: "foo@bar.com"},
|
||||||
|
},
|
||||||
|
acl: []workspace.WorkspaceObjectAccessControlResponse{
|
||||||
|
{
|
||||||
|
UserName: "foo@bar.com",
|
||||||
|
AllPermissions: []workspace.WorkspaceObjectPermission{
|
||||||
|
{PermissionLevel: "CAN_MANAGE"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
GroupName: "admins",
|
||||||
|
AllPermissions: []workspace.WorkspaceObjectPermission{
|
||||||
|
{PermissionLevel: "CAN_MANAGE"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
perms: []resources.Permission{
|
||||||
|
{Level: CAN_VIEW, UserName: "foo@bar.com"},
|
||||||
|
{Level: CAN_MANAGE, ServicePrincipalName: "sp.com"},
|
||||||
|
},
|
||||||
|
acl: []workspace.WorkspaceObjectAccessControlResponse{
|
||||||
|
{
|
||||||
|
UserName: "foo@bar.com",
|
||||||
|
AllPermissions: []workspace.WorkspaceObjectPermission{
|
||||||
|
{PermissionLevel: "CAN_READ"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
perms: []resources.Permission{
|
||||||
|
{Level: CAN_MANAGE, UserName: "foo@bar.com"},
|
||||||
|
},
|
||||||
|
acl: []workspace.WorkspaceObjectAccessControlResponse{
|
||||||
|
{
|
||||||
|
UserName: "foo@bar.com",
|
||||||
|
AllPermissions: []workspace.WorkspaceObjectPermission{
|
||||||
|
{PermissionLevel: "CAN_MANAGE"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
GroupName: "foo",
|
||||||
|
AllPermissions: []workspace.WorkspaceObjectPermission{
|
||||||
|
{PermissionLevel: "CAN_MANAGE"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: diag.Diagnostics{
|
||||||
|
{
|
||||||
|
Severity: diag.Warning,
|
||||||
|
Summary: "untracked permissions apply to target workspace path",
|
||||||
|
Detail: "The following permissions apply to the workspace folder at \"path\" but are not configured in the bundle:\n- level: CAN_MANAGE, group_name: foo\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
perms: []resources.Permission{
|
||||||
|
{Level: CAN_MANAGE, UserName: "foo@bar.com"},
|
||||||
|
},
|
||||||
|
acl: []workspace.WorkspaceObjectAccessControlResponse{
|
||||||
|
{
|
||||||
|
UserName: "foo2@bar.com",
|
||||||
|
AllPermissions: []workspace.WorkspaceObjectPermission{
|
||||||
|
{PermissionLevel: "CAN_MANAGE"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: diag.Diagnostics{
|
||||||
|
{
|
||||||
|
Severity: diag.Warning,
|
||||||
|
Summary: "untracked permissions apply to target workspace path",
|
||||||
|
Detail: "The following permissions apply to the workspace folder at \"path\" but are not configured in the bundle:\n- level: CAN_MANAGE, user_name: foo2@bar.com\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
wp := ObjectAclToResourcePermissions("path", tc.acl)
|
||||||
|
diags := wp.Compare(tc.perms)
|
||||||
|
require.Equal(t, tc.expected, diags)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -34,7 +34,7 @@ func giveAccessForWorkspaceRoot(ctx context.Context, b *bundle.Bundle) error {
|
||||||
permissions := make([]workspace.WorkspaceObjectAccessControlRequest, 0)
|
permissions := make([]workspace.WorkspaceObjectAccessControlRequest, 0)
|
||||||
|
|
||||||
for _, p := range b.Config.Permissions {
|
for _, p := range b.Config.Permissions {
|
||||||
level, err := getWorkspaceObjectPermissionLevel(p.Level)
|
level, err := GetWorkspaceObjectPermissionLevel(p.Level)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -65,7 +65,7 @@ func giveAccessForWorkspaceRoot(ctx context.Context, b *bundle.Bundle) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func getWorkspaceObjectPermissionLevel(bundlePermission string) (workspace.WorkspaceObjectPermissionLevel, error) {
|
func GetWorkspaceObjectPermissionLevel(bundlePermission string) (workspace.WorkspaceObjectPermissionLevel, error) {
|
||||||
switch bundlePermission {
|
switch bundlePermission {
|
||||||
case CAN_MANAGE:
|
case CAN_MANAGE:
|
||||||
return workspace.WorkspaceObjectPermissionLevelCanManage, nil
|
return workspace.WorkspaceObjectPermissionLevelCanManage, nil
|
||||||
|
|
Loading…
Reference in New Issue