mirror of https://github.com/databricks/cli.git
Compare commits
No commits in common. "845d23ac21b7a945714150ef68d48e6629a8bc6f" and "c92c67afc9755e0d3ae58f85c1ea427a2253d195" have entirely different histories.
845d23ac21
...
c92c67afc9
|
@ -5,8 +5,8 @@ import (
|
||||||
|
|
||||||
"github.com/databricks/cli/bundle"
|
"github.com/databricks/cli/bundle"
|
||||||
"github.com/databricks/cli/bundle/config"
|
"github.com/databricks/cli/bundle/config"
|
||||||
|
"github.com/databricks/cli/libs/auth"
|
||||||
"github.com/databricks/cli/libs/diag"
|
"github.com/databricks/cli/libs/diag"
|
||||||
"github.com/databricks/cli/libs/iamutil"
|
|
||||||
"github.com/databricks/cli/libs/tags"
|
"github.com/databricks/cli/libs/tags"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ func (m *populateCurrentUser) Apply(ctx context.Context, b *bundle.Bundle) diag.
|
||||||
}
|
}
|
||||||
|
|
||||||
b.Config.Workspace.CurrentUser = &config.User{
|
b.Config.Workspace.CurrentUser = &config.User{
|
||||||
ShortName: iamutil.GetShortUserName(me),
|
ShortName: auth.GetShortUserName(me),
|
||||||
User: me,
|
User: me,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,9 +6,9 @@ import (
|
||||||
|
|
||||||
"github.com/databricks/cli/bundle"
|
"github.com/databricks/cli/bundle"
|
||||||
"github.com/databricks/cli/bundle/config"
|
"github.com/databricks/cli/bundle/config"
|
||||||
|
"github.com/databricks/cli/libs/auth"
|
||||||
"github.com/databricks/cli/libs/diag"
|
"github.com/databricks/cli/libs/diag"
|
||||||
"github.com/databricks/cli/libs/dyn"
|
"github.com/databricks/cli/libs/dyn"
|
||||||
"github.com/databricks/cli/libs/iamutil"
|
|
||||||
"github.com/databricks/cli/libs/log"
|
"github.com/databricks/cli/libs/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -174,7 +174,7 @@ func (m *processTargetMode) Apply(ctx context.Context, b *bundle.Bundle) diag.Di
|
||||||
transformDevelopmentMode(ctx, b)
|
transformDevelopmentMode(ctx, b)
|
||||||
return diags
|
return diags
|
||||||
case config.Production:
|
case config.Production:
|
||||||
isPrincipal := iamutil.IsServicePrincipal(b.Config.Workspace.CurrentUser.User)
|
isPrincipal := auth.IsServicePrincipal(b.Config.Workspace.CurrentUser.UserName)
|
||||||
return validateProductionMode(ctx, b, isPrincipal)
|
return validateProductionMode(ctx, b, isPrincipal)
|
||||||
case "":
|
case "":
|
||||||
// No action
|
// No action
|
||||||
|
|
|
@ -30,44 +30,50 @@ func (m *setRunAs) Name() string {
|
||||||
return "SetRunAs"
|
return "SetRunAs"
|
||||||
}
|
}
|
||||||
|
|
||||||
func reportRunAsNotSupported(resourceType string, location dyn.Location, currentUser string, runAsUser string) diag.Diagnostics {
|
type errUnsupportedResourceTypeForRunAs struct {
|
||||||
return diag.Diagnostics{{
|
resourceType string
|
||||||
Summary: fmt.Sprintf("%s do not support a setting a run_as user that is different from the owner.\n"+
|
resourceLocation dyn.Location
|
||||||
"Current identity: %s. Run as identity: %s.\n"+
|
currentUser string
|
||||||
"See https://docs.databricks.com/dev-tools/bundles/run-as.html to learn more about the run_as property.", resourceType, currentUser, runAsUser),
|
runAsUser string
|
||||||
Locations: []dyn.Location{location},
|
|
||||||
Severity: diag.Error,
|
|
||||||
}}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateRunAs(b *bundle.Bundle) diag.Diagnostics {
|
func (e errUnsupportedResourceTypeForRunAs) Error() string {
|
||||||
diags := diag.Diagnostics{}
|
return fmt.Sprintf("%s are not supported when the current deployment user is different from the bundle's run_as identity. Please deploy as the run_as identity. Please refer to the documentation at https://docs.databricks.com/dev-tools/bundles/run-as.html for more details. Location of the unsupported resource: %s. Current identity: %s. Run as identity: %s", e.resourceType, e.resourceLocation, e.currentUser, e.runAsUser)
|
||||||
|
}
|
||||||
|
|
||||||
neitherSpecifiedErr := diag.Diagnostics{{
|
type errBothSpAndUserSpecified struct {
|
||||||
Summary: "run_as section must specify exactly one identity. Neither service_principal_name nor user_name is specified",
|
spName string
|
||||||
Locations: []dyn.Location{b.Config.GetLocation("run_as")},
|
spLoc dyn.Location
|
||||||
Severity: diag.Error,
|
userName string
|
||||||
}}
|
userLoc dyn.Location
|
||||||
|
}
|
||||||
|
|
||||||
// Fail fast if neither service_principal_name nor user_name are specified, but the
|
func (e errBothSpAndUserSpecified) Error() string {
|
||||||
|
return fmt.Sprintf("run_as section must specify exactly one identity. A service_principal_name %q is specified at %s. A user_name %q is defined at %s", e.spName, e.spLoc, e.userName, e.userLoc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateRunAs(b *bundle.Bundle) error {
|
||||||
|
neitherSpecifiedErr := fmt.Errorf("run_as section must specify exactly one identity. Neither service_principal_name nor user_name is specified at %s", b.Config.GetLocation("run_as"))
|
||||||
|
// Error if neither service_principal_name nor user_name are specified, but the
|
||||||
// run_as section is present.
|
// run_as section is present.
|
||||||
if b.Config.Value().Get("run_as").Kind() == dyn.KindNil {
|
if b.Config.Value().Get("run_as").Kind() == dyn.KindNil {
|
||||||
return neitherSpecifiedErr
|
return neitherSpecifiedErr
|
||||||
}
|
}
|
||||||
|
// Error if one or both of service_principal_name and user_name are specified,
|
||||||
// Fail fast if one or both of service_principal_name and user_name are specified,
|
|
||||||
// but with empty values.
|
// but with empty values.
|
||||||
runAs := b.Config.RunAs
|
if b.Config.RunAs.ServicePrincipalName == "" && b.Config.RunAs.UserName == "" {
|
||||||
if runAs.ServicePrincipalName == "" && runAs.UserName == "" {
|
|
||||||
return neitherSpecifiedErr
|
return neitherSpecifiedErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Error if both service_principal_name and user_name are specified
|
||||||
|
runAs := b.Config.RunAs
|
||||||
if runAs.UserName != "" && runAs.ServicePrincipalName != "" {
|
if runAs.UserName != "" && runAs.ServicePrincipalName != "" {
|
||||||
diags = diags.Extend(diag.Diagnostics{{
|
return errBothSpAndUserSpecified{
|
||||||
Summary: "run_as section cannot specify both user_name and service_principal_name",
|
spName: runAs.ServicePrincipalName,
|
||||||
Locations: []dyn.Location{b.Config.GetLocation("run_as")},
|
userName: runAs.UserName,
|
||||||
Severity: diag.Error,
|
spLoc: b.Config.GetLocation("run_as.service_principal_name"),
|
||||||
}})
|
userLoc: b.Config.GetLocation("run_as.user_name"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
identity := runAs.ServicePrincipalName
|
identity := runAs.ServicePrincipalName
|
||||||
|
@ -77,40 +83,40 @@ func validateRunAs(b *bundle.Bundle) diag.Diagnostics {
|
||||||
|
|
||||||
// All resources are supported if the run_as identity is the same as the current deployment identity.
|
// All resources are supported if the run_as identity is the same as the current deployment identity.
|
||||||
if identity == b.Config.Workspace.CurrentUser.UserName {
|
if identity == b.Config.Workspace.CurrentUser.UserName {
|
||||||
return diags
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DLT pipelines do not support run_as in the API.
|
// DLT pipelines do not support run_as in the API.
|
||||||
if len(b.Config.Resources.Pipelines) > 0 {
|
if len(b.Config.Resources.Pipelines) > 0 {
|
||||||
diags = diags.Extend(reportRunAsNotSupported(
|
return errUnsupportedResourceTypeForRunAs{
|
||||||
"pipelines",
|
resourceType: "pipelines",
|
||||||
b.Config.GetLocation("resources.pipelines"),
|
resourceLocation: b.Config.GetLocation("resources.pipelines"),
|
||||||
b.Config.Workspace.CurrentUser.UserName,
|
currentUser: b.Config.Workspace.CurrentUser.UserName,
|
||||||
identity,
|
runAsUser: identity,
|
||||||
))
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Model serving endpoints do not support run_as in the API.
|
// Model serving endpoints do not support run_as in the API.
|
||||||
if len(b.Config.Resources.ModelServingEndpoints) > 0 {
|
if len(b.Config.Resources.ModelServingEndpoints) > 0 {
|
||||||
diags = diags.Extend(reportRunAsNotSupported(
|
return errUnsupportedResourceTypeForRunAs{
|
||||||
"model_serving_endpoints",
|
resourceType: "model_serving_endpoints",
|
||||||
b.Config.GetLocation("resources.model_serving_endpoints"),
|
resourceLocation: b.Config.GetLocation("resources.model_serving_endpoints"),
|
||||||
b.Config.Workspace.CurrentUser.UserName,
|
currentUser: b.Config.Workspace.CurrentUser.UserName,
|
||||||
identity,
|
runAsUser: identity,
|
||||||
))
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Monitors do not support run_as in the API.
|
// Monitors do not support run_as in the API.
|
||||||
if len(b.Config.Resources.QualityMonitors) > 0 {
|
if len(b.Config.Resources.QualityMonitors) > 0 {
|
||||||
diags = diags.Extend(reportRunAsNotSupported(
|
return errUnsupportedResourceTypeForRunAs{
|
||||||
"quality_monitors",
|
resourceType: "quality_monitors",
|
||||||
b.Config.GetLocation("resources.quality_monitors"),
|
resourceLocation: b.Config.GetLocation("resources.quality_monitors"),
|
||||||
b.Config.Workspace.CurrentUser.UserName,
|
currentUser: b.Config.Workspace.CurrentUser.UserName,
|
||||||
identity,
|
runAsUser: identity,
|
||||||
))
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return diags
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func setRunAsForJobs(b *bundle.Bundle) {
|
func setRunAsForJobs(b *bundle.Bundle) {
|
||||||
|
@ -181,9 +187,8 @@ func (m *setRunAs) Apply(_ context.Context, b *bundle.Bundle) diag.Diagnostics {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assert the run_as configuration is valid in the context of the bundle
|
// Assert the run_as configuration is valid in the context of the bundle
|
||||||
diags := validateRunAs(b)
|
if err := validateRunAs(b); err != nil {
|
||||||
if diags.HasError() {
|
return diag.FromErr(err)
|
||||||
return diags
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setRunAsForJobs(b)
|
setRunAsForJobs(b)
|
||||||
|
|
|
@ -188,8 +188,11 @@ func TestRunAsErrorForUnsupportedResources(t *testing.T) {
|
||||||
Config: *r,
|
Config: *r,
|
||||||
}
|
}
|
||||||
diags := bundle.Apply(context.Background(), b, SetRunAs())
|
diags := bundle.Apply(context.Background(), b, SetRunAs())
|
||||||
assert.Contains(t, diags.Error().Error(), "do not support a setting a run_as user that is different from the owner.\n"+
|
assert.Equal(t, diags.Error().Error(), errUnsupportedResourceTypeForRunAs{
|
||||||
"Current identity: alice. Run as identity: bob.\n"+
|
resourceType: rt,
|
||||||
"See https://docs.databricks.com/dev-tools/bundles/run-as.html to learn more about the run_as property.", rt)
|
resourceLocation: dyn.Location{},
|
||||||
|
currentUser: "alice",
|
||||||
|
runAsUser: "bob",
|
||||||
|
}.Error(), "expected run_as with a different identity than the current deployment user to not supported for resources of type: %s", rt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,9 @@ package files
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
|
||||||
|
|
||||||
"github.com/databricks/cli/bundle"
|
"github.com/databricks/cli/bundle"
|
||||||
"github.com/databricks/cli/bundle/permissions"
|
|
||||||
"github.com/databricks/cli/libs/cmdio"
|
"github.com/databricks/cli/libs/cmdio"
|
||||||
"github.com/databricks/cli/libs/diag"
|
"github.com/databricks/cli/libs/diag"
|
||||||
"github.com/databricks/cli/libs/log"
|
"github.com/databricks/cli/libs/log"
|
||||||
|
@ -38,9 +35,6 @@ func (m *upload) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
|
||||||
|
|
||||||
b.Files, err = sync.RunOnce(ctx)
|
b.Files, err = sync.RunOnce(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, fs.ErrPermission) {
|
|
||||||
return permissions.ReportPossiblePermissionDenied(ctx, b, b.Config.Workspace.FilePath)
|
|
||||||
}
|
|
||||||
return diag.FromErr(err)
|
return diag.FromErr(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,8 @@ package lock
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"io/fs"
|
|
||||||
|
|
||||||
"github.com/databricks/cli/bundle"
|
"github.com/databricks/cli/bundle"
|
||||||
"github.com/databricks/cli/bundle/permissions"
|
|
||||||
"github.com/databricks/cli/libs/diag"
|
"github.com/databricks/cli/libs/diag"
|
||||||
"github.com/databricks/cli/libs/filer"
|
"github.com/databricks/cli/libs/filer"
|
||||||
"github.com/databricks/cli/libs/locker"
|
"github.com/databricks/cli/libs/locker"
|
||||||
|
@ -53,17 +51,12 @@ func (m *acquire) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf(ctx, "Failed to acquire deployment lock: %v", err)
|
log.Errorf(ctx, "Failed to acquire deployment lock: %v", err)
|
||||||
|
|
||||||
if errors.Is(err, fs.ErrPermission) {
|
|
||||||
return permissions.ReportPossiblePermissionDenied(ctx, b, b.Config.Workspace.StatePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
notExistsError := filer.NoSuchDirectoryError{}
|
notExistsError := filer.NoSuchDirectoryError{}
|
||||||
if errors.As(err, ¬ExistsError) {
|
if errors.As(err, ¬ExistsError) {
|
||||||
// If we get a "doesn't exist" error from the API this indicates
|
// If we get a "doesn't exist" error from the API this indicates
|
||||||
// we either don't have permissions or the path is invalid.
|
// we either don't have permissions or the path is invalid.
|
||||||
return permissions.ReportPossiblePermissionDenied(ctx, b, b.Config.Workspace.StatePath)
|
return diag.Errorf("cannot write to deployment root (this can indicate a previous deploy was done with a different identity): %s", b.Config.Workspace.RootPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
return diag.FromErr(err)
|
return diag.FromErr(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/databricks/cli/bundle"
|
"github.com/databricks/cli/bundle"
|
||||||
"github.com/databricks/cli/bundle/permissions"
|
|
||||||
"github.com/databricks/cli/libs/diag"
|
"github.com/databricks/cli/libs/diag"
|
||||||
"github.com/databricks/cli/libs/log"
|
"github.com/databricks/cli/libs/log"
|
||||||
"github.com/hashicorp/terraform-exec/tfexec"
|
"github.com/hashicorp/terraform-exec/tfexec"
|
||||||
|
@ -35,10 +34,6 @@ func (w *apply) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
|
||||||
// Apply terraform according to the computed plan
|
// Apply terraform according to the computed plan
|
||||||
err := tf.Apply(ctx, tfexec.DirOrPlan(b.Plan.Path))
|
err := tf.Apply(ctx, tfexec.DirOrPlan(b.Plan.Path))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
diags := permissions.TryExtendTerraformPermissionError(ctx, b, err)
|
|
||||||
if diags != nil {
|
|
||||||
return diags
|
|
||||||
}
|
|
||||||
return diag.Errorf("terraform apply: %v", err)
|
return diag.Errorf("terraform apply: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,7 @@ func (clusterConverter) Convert(ctx context.Context, key string, vin dyn.Value,
|
||||||
|
|
||||||
// Configure permissions for this resource.
|
// Configure permissions for this resource.
|
||||||
if permissions := convertPermissionsResource(ctx, vin); permissions != nil {
|
if permissions := convertPermissionsResource(ctx, vin); permissions != nil {
|
||||||
permissions.ClusterId = fmt.Sprintf("${databricks_cluster.%s.id}", key)
|
permissions.JobId = fmt.Sprintf("${databricks_cluster.%s.id}", key)
|
||||||
out.Permissions["cluster_"+key] = permissions
|
out.Permissions["cluster_"+key] = permissions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -81,7 +81,7 @@ func TestConvertCluster(t *testing.T) {
|
||||||
|
|
||||||
// Assert equality on the permissions
|
// Assert equality on the permissions
|
||||||
assert.Equal(t, &schema.ResourcePermissions{
|
assert.Equal(t, &schema.ResourcePermissions{
|
||||||
ClusterId: "${databricks_cluster.my_cluster.id}",
|
JobId: "${databricks_cluster.my_cluster.id}",
|
||||||
AccessControl: []schema.ResourcePermissionsAccessControl{
|
AccessControl: []schema.ResourcePermissionsAccessControl{
|
||||||
{
|
{
|
||||||
PermissionLevel: "CAN_RUN",
|
PermissionLevel: "CAN_RUN",
|
||||||
|
|
|
@ -1,110 +0,0 @@
|
||||||
package permissions
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/databricks/cli/bundle"
|
|
||||||
"github.com/databricks/cli/libs/diag"
|
|
||||||
"github.com/databricks/cli/libs/dyn"
|
|
||||||
"github.com/databricks/cli/libs/set"
|
|
||||||
)
|
|
||||||
|
|
||||||
type permissionDiagnostics struct{}
|
|
||||||
|
|
||||||
func PermissionDiagnostics() bundle.Mutator {
|
|
||||||
return &permissionDiagnostics{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *permissionDiagnostics) Name() string {
|
|
||||||
return "CheckPermissions"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *permissionDiagnostics) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
|
|
||||||
if len(b.Config.Permissions) == 0 {
|
|
||||||
// Only warn if there is an explicit top-level permissions section
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
canManageBundle, _ := analyzeBundlePermissions(b)
|
|
||||||
if canManageBundle {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return diag.Diagnostics{{
|
|
||||||
Severity: diag.Warning,
|
|
||||||
Summary: fmt.Sprintf("permissions section should include %s or one of their groups with CAN_MANAGE permissions", b.Config.Workspace.CurrentUser.UserName),
|
|
||||||
Locations: []dyn.Location{b.Config.GetLocation("permissions")},
|
|
||||||
ID: diag.PermissionNotIncluded,
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
|
|
||||||
// analyzeBundlePermissions analyzes the top-level permissions of the bundle.
|
|
||||||
// This permission set is important since it determines the permissions of the
|
|
||||||
// target workspace folder.
|
|
||||||
//
|
|
||||||
// Returns:
|
|
||||||
// - isManager: true if the current user is can manage the bundle resources.
|
|
||||||
// - assistance: advice on who to contact as to manage this project
|
|
||||||
func analyzeBundlePermissions(b *bundle.Bundle) (bool, string) {
|
|
||||||
canManageBundle := false
|
|
||||||
otherManagers := set.NewSet[string]()
|
|
||||||
if b.Config.RunAs != nil && b.Config.RunAs.UserName != "" && b.Config.RunAs.UserName != b.Config.Workspace.CurrentUser.UserName {
|
|
||||||
// The run_as user is another human that could be contacted
|
|
||||||
// about this bundle.
|
|
||||||
otherManagers.Add(b.Config.RunAs.UserName)
|
|
||||||
}
|
|
||||||
|
|
||||||
currentUser := b.Config.Workspace.CurrentUser.UserName
|
|
||||||
targetPermissions := b.Config.Permissions
|
|
||||||
for _, p := range targetPermissions {
|
|
||||||
if p.Level != CAN_MANAGE {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.UserName == currentUser || p.ServicePrincipalName == currentUser {
|
|
||||||
canManageBundle = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if isGroupOfCurrentUser(b, p.GroupName) {
|
|
||||||
canManageBundle = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Permission doesn't apply to current user; add to otherManagers
|
|
||||||
otherManager := p.UserName
|
|
||||||
if otherManager == "" {
|
|
||||||
otherManager = p.GroupName
|
|
||||||
}
|
|
||||||
if otherManager == "" {
|
|
||||||
// Skip service principals
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
otherManagers.Add(otherManager)
|
|
||||||
}
|
|
||||||
|
|
||||||
assistance := "For assistance, contact the owners of this project."
|
|
||||||
if otherManagers.Size() > 0 {
|
|
||||||
list := otherManagers.Values()
|
|
||||||
sort.Strings(list)
|
|
||||||
assistance = fmt.Sprintf(
|
|
||||||
"For assistance, users or groups with appropriate permissions may include: %s.",
|
|
||||||
strings.Join(list, ", "),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return canManageBundle, assistance
|
|
||||||
}
|
|
||||||
|
|
||||||
func isGroupOfCurrentUser(b *bundle.Bundle, groupName string) bool {
|
|
||||||
currentUserGroups := b.Config.Workspace.CurrentUser.User.Groups
|
|
||||||
|
|
||||||
for _, g := range currentUserGroups {
|
|
||||||
if g.Display == groupName {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
|
@ -1,52 +0,0 @@
|
||||||
package permissions_test
|
|
||||||
|
|
||||||
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/service/iam"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestPermissionDiagnosticsApplySuccess(t *testing.T) {
|
|
||||||
b := mockBundle([]resources.Permission{
|
|
||||||
{Level: "CAN_MANAGE", UserName: "testuser@databricks.com"},
|
|
||||||
})
|
|
||||||
|
|
||||||
diags := permissions.PermissionDiagnostics().Apply(context.Background(), b)
|
|
||||||
require.NoError(t, diags.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPermissionDiagnosticsApplyFail(t *testing.T) {
|
|
||||||
b := mockBundle([]resources.Permission{
|
|
||||||
{Level: "CAN_VIEW", UserName: "testuser@databricks.com"},
|
|
||||||
})
|
|
||||||
|
|
||||||
diags := permissions.PermissionDiagnostics().Apply(context.Background(), b)
|
|
||||||
require.Equal(t, diags[0].Severity, diag.Warning)
|
|
||||||
require.Contains(t, diags[0].Summary, "permissions section should include testuser@databricks.com or one of their groups with CAN_MANAGE permissions")
|
|
||||||
}
|
|
||||||
|
|
||||||
func mockBundle(permissions []resources.Permission) *bundle.Bundle {
|
|
||||||
return &bundle.Bundle{
|
|
||||||
Config: config.Root{
|
|
||||||
Workspace: config.Workspace{
|
|
||||||
CurrentUser: &config.User{
|
|
||||||
User: &iam.User{
|
|
||||||
UserName: "testuser@databricks.com",
|
|
||||||
DisplayName: "Test User",
|
|
||||||
Groups: []iam.ComplexValue{
|
|
||||||
{Display: "testgroup"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Permissions: permissions,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,52 +0,0 @@
|
||||||
package permissions
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/databricks/cli/bundle"
|
|
||||||
"github.com/databricks/cli/libs/diag"
|
|
||||||
"github.com/databricks/cli/libs/iamutil"
|
|
||||||
"github.com/databricks/cli/libs/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ReportPossiblePermissionDenied generates a diagnostic message when a permission denied error is encountered.
|
|
||||||
//
|
|
||||||
// Note that since the workspace API doesn't always distinguish between permission denied and path errors,
|
|
||||||
// we must treat this as a "possible permission error". See acquire.go for more about this.
|
|
||||||
func ReportPossiblePermissionDenied(ctx context.Context, b *bundle.Bundle, path string) diag.Diagnostics {
|
|
||||||
log.Errorf(ctx, "Failed to update, encountered possible permission error: %v", path)
|
|
||||||
|
|
||||||
me := b.Config.Workspace.CurrentUser.User
|
|
||||||
userName := me.UserName
|
|
||||||
if iamutil.IsServicePrincipal(me) {
|
|
||||||
userName = me.DisplayName
|
|
||||||
}
|
|
||||||
canManageBundle, assistance := analyzeBundlePermissions(b)
|
|
||||||
|
|
||||||
if !canManageBundle {
|
|
||||||
return diag.Diagnostics{{
|
|
||||||
Summary: fmt.Sprintf("unable to deploy to %s as %s.\n"+
|
|
||||||
"Please make sure the current user or one of their groups is listed under the permissions of this bundle.\n"+
|
|
||||||
"%s\n"+
|
|
||||||
"They may need to redeploy the bundle to apply the new permissions.\n"+
|
|
||||||
"Please refer to https://docs.databricks.com/dev-tools/bundles/permissions.html for more on managing permissions.",
|
|
||||||
path, userName, assistance),
|
|
||||||
Severity: diag.Error,
|
|
||||||
ID: diag.PathPermissionDenied,
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
|
|
||||||
// According databricks.yml, the current user has the right permissions.
|
|
||||||
// But we're still seeing permission errors. So someone else will need
|
|
||||||
// to redeploy the bundle with the right set of permissions.
|
|
||||||
return diag.Diagnostics{{
|
|
||||||
Summary: fmt.Sprintf("unable to deploy to %s as %s. Cannot apply local deployment permissions.\n"+
|
|
||||||
"%s\n"+
|
|
||||||
"They can redeploy the project to apply the latest set of permissions.\n"+
|
|
||||||
"Please refer to https://docs.databricks.com/dev-tools/bundles/permissions.html for more on managing permissions.",
|
|
||||||
path, userName, assistance),
|
|
||||||
Severity: diag.Error,
|
|
||||||
ID: diag.CannotChangePathPermissions,
|
|
||||||
}}
|
|
||||||
}
|
|
|
@ -1,76 +0,0 @@
|
||||||
package permissions_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/databricks/cli/bundle/config/resources"
|
|
||||||
"github.com/databricks/cli/bundle/permissions"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestPermissionsReportPermissionDeniedWithGroup(t *testing.T) {
|
|
||||||
b := mockBundle([]resources.Permission{
|
|
||||||
{Level: "CAN_MANAGE", GroupName: "testgroup"},
|
|
||||||
})
|
|
||||||
|
|
||||||
diags := permissions.ReportPossiblePermissionDenied(context.Background(), b, "testpath")
|
|
||||||
expected := "EPERM3: unable to deploy to testpath as testuser@databricks.com. Cannot apply local deployment permissions.\n" +
|
|
||||||
"For assistance, contact the owners of this project.\n" +
|
|
||||||
"They can redeploy the project to apply the latest set of permissions.\n" +
|
|
||||||
"Please refer to https://docs.databricks.com/dev-tools/bundles/permissions.html for more on managing permissions."
|
|
||||||
require.ErrorContains(t, diags.Error(), expected)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPermissionsReportPermissionDeniedWithOtherGroup(t *testing.T) {
|
|
||||||
b := mockBundle([]resources.Permission{
|
|
||||||
{Level: "CAN_MANAGE", GroupName: "othergroup"},
|
|
||||||
})
|
|
||||||
|
|
||||||
diags := permissions.ReportPossiblePermissionDenied(context.Background(), b, "testpath")
|
|
||||||
expected := "EPERM1: unable to deploy to testpath as testuser@databricks.com.\n" +
|
|
||||||
"Please make sure the current user or one of their groups is listed under the permissions of this bundle.\n" +
|
|
||||||
"For assistance, users or groups with appropriate permissions may include: othergroup.\n" +
|
|
||||||
"They may need to redeploy the bundle to apply the new permissions.\n" +
|
|
||||||
"Please refer to https://docs.databricks.com/dev-tools/bundles/permissions.html for more on managing permissions."
|
|
||||||
require.ErrorContains(t, diags.Error(), expected)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPermissionsReportPermissionDeniedWithoutPermission(t *testing.T) {
|
|
||||||
b := mockBundle([]resources.Permission{
|
|
||||||
{Level: "CAN_VIEW", UserName: "testuser@databricks.com"},
|
|
||||||
})
|
|
||||||
|
|
||||||
diags := permissions.ReportPossiblePermissionDenied(context.Background(), b, "testpath")
|
|
||||||
expected := "EPERM1: unable to deploy to testpath as testuser@databricks.com.\n" +
|
|
||||||
"Please make sure the current user or one of their groups is listed under the permissions of this bundle.\n" +
|
|
||||||
"For assistance, contact the owners of this project.\n" +
|
|
||||||
"They may need to redeploy the bundle to apply the new permissions.\n" +
|
|
||||||
"Please refer to https://docs.databricks.com/dev-tools/bundles/permissions.html for more on managing permissions."
|
|
||||||
require.ErrorContains(t, diags.Error(), expected)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPermissionsReportPermissionDeniedNilPermission(t *testing.T) {
|
|
||||||
b := mockBundle(nil)
|
|
||||||
|
|
||||||
diags := permissions.ReportPossiblePermissionDenied(context.Background(), b, "testpath")
|
|
||||||
expected := "EPERM1: unable to deploy to testpath as testuser@databricks.com.\n" +
|
|
||||||
"Please make sure the current user or one of their groups is listed under the permissions of this bundle.\n" +
|
|
||||||
"For assistance, contact the owners of this project.\n" +
|
|
||||||
"They may need to redeploy the bundle to apply the new permissions.\n" +
|
|
||||||
"Please refer to https://docs.databricks.com/dev-tools/bundles/permissions.html for more on managing permissions"
|
|
||||||
require.ErrorContains(t, diags.Error(), expected)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPermissionsReportFindOtherOwners(t *testing.T) {
|
|
||||||
b := mockBundle([]resources.Permission{
|
|
||||||
{Level: "CAN_MANAGE", GroupName: "testgroup"},
|
|
||||||
{Level: "CAN_MANAGE", UserName: "alice@databricks.com"},
|
|
||||||
})
|
|
||||||
|
|
||||||
diags := permissions.ReportPossiblePermissionDenied(context.Background(), b, "testpath")
|
|
||||||
require.ErrorContains(t, diags.Error(), "EPERM3: unable to deploy to testpath as testuser@databricks.com. Cannot apply local deployment permissions.\n"+
|
|
||||||
"For assistance, users or groups with appropriate permissions may include: alice@databricks.com.\n"+
|
|
||||||
"They can redeploy the project to apply the latest set of permissions.\n"+
|
|
||||||
"Please refer to https://docs.databricks.com/dev-tools/bundles/permissions.html for more on managing permissions.")
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
package permissions
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/databricks/cli/bundle"
|
|
||||||
"github.com/databricks/cli/libs/diag"
|
|
||||||
"github.com/databricks/cli/libs/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TryExtendTerraformPermissionError(ctx context.Context, b *bundle.Bundle, err error) diag.Diagnostics {
|
|
||||||
_, assistance := analyzeBundlePermissions(b)
|
|
||||||
|
|
||||||
// In a best-effort attempt to provide actionable error messages, we match
|
|
||||||
// against a few specific error messages that come from the Jobs and Pipelines API.
|
|
||||||
// For matching errors we provide a more specific error message that includes
|
|
||||||
// details on how to resolve the issue.
|
|
||||||
if !strings.Contains(err.Error(), "cannot update permissions") &&
|
|
||||||
!strings.Contains(err.Error(), "permissions on pipeline") &&
|
|
||||||
!strings.Contains(err.Error(), "cannot read permissions") &&
|
|
||||||
!strings.Contains(err.Error(), "cannot set run_as to user") {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Errorf(ctx, "Terraform error during deployment: %v", err.Error())
|
|
||||||
|
|
||||||
// Best-effort attempt to extract the resource name from the error message.
|
|
||||||
re := regexp.MustCompile(`databricks_(\w*)\.(\w*)`)
|
|
||||||
match := re.FindStringSubmatch(err.Error())
|
|
||||||
resource := "resource"
|
|
||||||
if len(match) > 1 {
|
|
||||||
resource = match[2]
|
|
||||||
}
|
|
||||||
|
|
||||||
return diag.Diagnostics{{
|
|
||||||
Summary: fmt.Sprintf("permission denied creating or updating %s.\n"+
|
|
||||||
"%s\n"+
|
|
||||||
"They can redeploy the project to apply the latest set of permissions.\n"+
|
|
||||||
"Please refer to https://docs.databricks.com/dev-tools/bundles/permissions.html for more on managing permissions.",
|
|
||||||
resource, assistance),
|
|
||||||
Severity: diag.Error,
|
|
||||||
ID: diag.ResourcePermissionDenied,
|
|
||||||
}}
|
|
||||||
}
|
|
|
@ -1,97 +0,0 @@
|
||||||
package permissions_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/databricks/cli/bundle/config/resources"
|
|
||||||
"github.com/databricks/cli/bundle/permissions"
|
|
||||||
"github.com/databricks/databricks-sdk-go/service/jobs"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestTryExtendTerraformPermissionError1(t *testing.T) {
|
|
||||||
ctx := context.Background()
|
|
||||||
b := mockBundle([]resources.Permission{
|
|
||||||
{Level: "CAN_MANAGE", UserName: "alice@databricks.com"},
|
|
||||||
})
|
|
||||||
err := permissions.TryExtendTerraformPermissionError(ctx, b, errors.New("Error: terraform apply: exit status 1\n"+
|
|
||||||
"\n"+
|
|
||||||
"Error: cannot update permissions: ...\n"+
|
|
||||||
"\n"+
|
|
||||||
" with databricks_pipeline.my_project_pipeline,\n"+
|
|
||||||
" on bundle.tf.json line 39, in resource.databricks_pipeline.my_project_pipeline:\n"+
|
|
||||||
" 39: }")).Error()
|
|
||||||
|
|
||||||
expected := "EPERM2: permission denied creating or updating my_project_pipeline.\n" +
|
|
||||||
"For assistance, users or groups with appropriate permissions may include: alice@databricks.com.\n" +
|
|
||||||
"They can redeploy the project to apply the latest set of permissions.\n" +
|
|
||||||
"Please refer to https://docs.databricks.com/dev-tools/bundles/permissions.html for more on managing permissions"
|
|
||||||
|
|
||||||
require.ErrorContains(t, err, expected)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTryExtendTerraformPermissionError2(t *testing.T) {
|
|
||||||
ctx := context.Background()
|
|
||||||
b := mockBundle([]resources.Permission{
|
|
||||||
{Level: "CAN_MANAGE", UserName: "alice@databricks.com"},
|
|
||||||
{Level: "CAN_MANAGE", UserName: "bob@databricks.com"},
|
|
||||||
})
|
|
||||||
err := permissions.TryExtendTerraformPermissionError(ctx, b, errors.New("Error: terraform apply: exit status 1\n"+
|
|
||||||
"\n"+
|
|
||||||
"Error: cannot read pipeline: User xyz does not have View permissions on pipeline 4521dbb6-42aa-418c-b94d-b5f4859a3454.\n"+
|
|
||||||
"\n"+
|
|
||||||
" with databricks_pipeline.my_project_pipeline,\n"+
|
|
||||||
" on bundle.tf.json line 39, in resource.databricks_pipeline.my_project_pipeline:\n"+
|
|
||||||
" 39: }")).Error()
|
|
||||||
|
|
||||||
expected := "EPERM2: permission denied creating or updating my_project_pipeline.\n" +
|
|
||||||
"For assistance, users or groups with appropriate permissions may include: alice@databricks.com, bob@databricks.com.\n" +
|
|
||||||
"They can redeploy the project to apply the latest set of permissions.\n" +
|
|
||||||
"Please refer to https://docs.databricks.com/dev-tools/bundles/permissions.html for more on managing permissions."
|
|
||||||
require.ErrorContains(t, err, expected)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTryExtendTerraformPermissionError3(t *testing.T) {
|
|
||||||
ctx := context.Background()
|
|
||||||
b := mockBundle([]resources.Permission{
|
|
||||||
{Level: "CAN_MANAGE", UserName: "testuser@databricks.com"},
|
|
||||||
})
|
|
||||||
err := permissions.TryExtendTerraformPermissionError(ctx, b, errors.New("Error: terraform apply: exit status 1\n"+
|
|
||||||
"\n"+
|
|
||||||
"Error: cannot read permissions: 1706906c-c0a2-4c25-9f57-3a7aa3cb8b90 does not have Owner permissions on Job with ID: ElasticJobId(28263044278868). Please contact the owner or an administrator for access.\n"+
|
|
||||||
"\n"+
|
|
||||||
" with databricks_pipeline.my_project_pipeline,\n"+
|
|
||||||
" on bundle.tf.json line 39, in resource.databricks_pipeline.my_project_pipeline:\n"+
|
|
||||||
" 39: }")).Error()
|
|
||||||
|
|
||||||
expected := "EPERM2: permission denied creating or updating my_project_pipeline.\n" +
|
|
||||||
"For assistance, contact the owners of this project.\n" +
|
|
||||||
"They can redeploy the project to apply the latest set of permissions.\n" +
|
|
||||||
"Please refer to https://docs.databricks.com/dev-tools/bundles/permissions.html for more on managing permissions."
|
|
||||||
require.ErrorContains(t, err, expected)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTryExtendTerraformPermissionErrorNotOwner(t *testing.T) {
|
|
||||||
ctx := context.Background()
|
|
||||||
b := mockBundle([]resources.Permission{
|
|
||||||
{Level: "CAN_MANAGE", GroupName: "data_team@databricks.com"},
|
|
||||||
})
|
|
||||||
b.Config.RunAs = &jobs.JobRunAs{
|
|
||||||
UserName: "testuser@databricks.com",
|
|
||||||
}
|
|
||||||
err := permissions.TryExtendTerraformPermissionError(ctx, b, errors.New("Error: terraform apply: exit status 1\n"+
|
|
||||||
"\n"+
|
|
||||||
"Error: cannot read pipeline: User xyz does not have View permissions on pipeline 4521dbb6-42aa-418c-b94d-b5f4859a3454.\n"+
|
|
||||||
"\n"+
|
|
||||||
" with databricks_pipeline.my_project_pipeline,\n"+
|
|
||||||
" on bundle.tf.json line 39, in resource.databricks_pipeline.my_project_pipeline:\n"+
|
|
||||||
" 39: }")).Error()
|
|
||||||
|
|
||||||
expected := "EPERM2: permission denied creating or updating my_project_pipeline.\n" +
|
|
||||||
"For assistance, users or groups with appropriate permissions may include: data_team@databricks.com.\n" +
|
|
||||||
"They can redeploy the project to apply the latest set of permissions.\n" +
|
|
||||||
"Please refer to https://docs.databricks.com/dev-tools/bundles/permissions.html for more on managing permissions."
|
|
||||||
require.ErrorContains(t, err, expected)
|
|
||||||
}
|
|
|
@ -62,8 +62,6 @@ func Initialize() bundle.Mutator {
|
||||||
"workspace",
|
"workspace",
|
||||||
"variables",
|
"variables",
|
||||||
),
|
),
|
||||||
// Provide permission config errors & warnings after initializing all variables
|
|
||||||
permissions.PermissionDiagnostics(),
|
|
||||||
mutator.SetRunAs(),
|
mutator.SetRunAs(),
|
||||||
mutator.OverrideCompute(),
|
mutator.OverrideCompute(),
|
||||||
mutator.ProcessTargetMode(),
|
mutator.ProcessTargetMode(),
|
||||||
|
|
|
@ -3,6 +3,7 @@ package config_tests
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/databricks/cli/bundle"
|
"github.com/databricks/cli/bundle"
|
||||||
|
@ -112,9 +113,8 @@ func TestRunAsErrorForPipelines(t *testing.T) {
|
||||||
diags := bundle.Apply(ctx, b, mutator.SetRunAs())
|
diags := bundle.Apply(ctx, b, mutator.SetRunAs())
|
||||||
err := diags.Error()
|
err := diags.Error()
|
||||||
|
|
||||||
assert.ErrorContains(t, err, "pipelines do not support a setting a run_as user that is different from the owner.\n"+
|
configPath := filepath.FromSlash("run_as/not_allowed/pipelines/databricks.yml")
|
||||||
"Current identity: jane@doe.com. Run as identity: my_service_principal.\n"+
|
assert.EqualError(t, err, fmt.Sprintf("pipelines are not supported when the current deployment user is different from the bundle's run_as identity. Please deploy as the run_as identity. Please refer to the documentation at https://docs.databricks.com/dev-tools/bundles/run-as.html for more details. Location of the unsupported resource: %s:14:5. Current identity: jane@doe.com. Run as identity: my_service_principal", configPath))
|
||||||
"See https://docs")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRunAsNoErrorForPipelines(t *testing.T) {
|
func TestRunAsNoErrorForPipelines(t *testing.T) {
|
||||||
|
@ -152,9 +152,8 @@ func TestRunAsErrorForModelServing(t *testing.T) {
|
||||||
diags := bundle.Apply(ctx, b, mutator.SetRunAs())
|
diags := bundle.Apply(ctx, b, mutator.SetRunAs())
|
||||||
err := diags.Error()
|
err := diags.Error()
|
||||||
|
|
||||||
assert.ErrorContains(t, err, "model_serving_endpoints do not support a setting a run_as user that is different from the owner.\n"+
|
configPath := filepath.FromSlash("run_as/not_allowed/model_serving/databricks.yml")
|
||||||
"Current identity: jane@doe.com. Run as identity: my_service_principal.\n"+
|
assert.EqualError(t, err, fmt.Sprintf("model_serving_endpoints are not supported when the current deployment user is different from the bundle's run_as identity. Please deploy as the run_as identity. Please refer to the documentation at https://docs.databricks.com/dev-tools/bundles/run-as.html for more details. Location of the unsupported resource: %s:14:5. Current identity: jane@doe.com. Run as identity: my_service_principal", configPath))
|
||||||
"See https://docs")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRunAsNoErrorForModelServingEndpoints(t *testing.T) {
|
func TestRunAsNoErrorForModelServingEndpoints(t *testing.T) {
|
||||||
|
@ -192,7 +191,8 @@ func TestRunAsErrorWhenBothUserAndSpSpecified(t *testing.T) {
|
||||||
diags := bundle.Apply(ctx, b, mutator.SetRunAs())
|
diags := bundle.Apply(ctx, b, mutator.SetRunAs())
|
||||||
err := diags.Error()
|
err := diags.Error()
|
||||||
|
|
||||||
assert.ErrorContains(t, err, "run_as section cannot specify both user_name and service_principal_name")
|
configPath := filepath.FromSlash("run_as/not_allowed/both_sp_and_user/databricks.yml")
|
||||||
|
assert.EqualError(t, err, fmt.Sprintf("run_as section must specify exactly one identity. A service_principal_name \"my_service_principal\" is specified at %s:6:27. A user_name \"my_user_name\" is defined at %s:7:14", configPath, configPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRunAsErrorNeitherUserOrSpSpecified(t *testing.T) {
|
func TestRunAsErrorNeitherUserOrSpSpecified(t *testing.T) {
|
||||||
|
@ -202,19 +202,19 @@ func TestRunAsErrorNeitherUserOrSpSpecified(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "empty_run_as",
|
name: "empty_run_as",
|
||||||
err: "run_as section must specify exactly one identity. Neither service_principal_name nor user_name is specified",
|
err: fmt.Sprintf("run_as section must specify exactly one identity. Neither service_principal_name nor user_name is specified at %s:4:8", filepath.FromSlash("run_as/not_allowed/neither_sp_nor_user/empty_run_as/databricks.yml")),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "empty_sp",
|
name: "empty_sp",
|
||||||
err: "run_as section must specify exactly one identity. Neither service_principal_name nor user_name is specified",
|
err: fmt.Sprintf("run_as section must specify exactly one identity. Neither service_principal_name nor user_name is specified at %s:5:3", filepath.FromSlash("run_as/not_allowed/neither_sp_nor_user/empty_sp/databricks.yml")),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "empty_user",
|
name: "empty_user",
|
||||||
err: "run_as section must specify exactly one identity. Neither service_principal_name nor user_name is specified",
|
err: fmt.Sprintf("run_as section must specify exactly one identity. Neither service_principal_name nor user_name is specified at %s:5:3", filepath.FromSlash("run_as/not_allowed/neither_sp_nor_user/empty_user/databricks.yml")),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "empty_user_and_sp",
|
name: "empty_user_and_sp",
|
||||||
err: "run_as section must specify exactly one identity. Neither service_principal_name nor user_name is specified",
|
err: fmt.Sprintf("run_as section must specify exactly one identity. Neither service_principal_name nor user_name is specified at %s:5:3", filepath.FromSlash("run_as/not_allowed/neither_sp_nor_user/empty_user_and_sp/databricks.yml")),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -257,7 +257,8 @@ func TestRunAsErrorNeitherUserOrSpSpecifiedAtTargetOverride(t *testing.T) {
|
||||||
diags := bundle.Apply(ctx, b, mutator.SetRunAs())
|
diags := bundle.Apply(ctx, b, mutator.SetRunAs())
|
||||||
err := diags.Error()
|
err := diags.Error()
|
||||||
|
|
||||||
assert.EqualError(t, err, "run_as section must specify exactly one identity. Neither service_principal_name nor user_name is specified")
|
configPath := filepath.FromSlash("run_as/not_allowed/neither_sp_nor_user/override/override.yml")
|
||||||
|
assert.EqualError(t, err, fmt.Sprintf("run_as section must specify exactly one identity. Neither service_principal_name nor user_name is specified at %s:4:12", configPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLegacyRunAs(t *testing.T) {
|
func TestLegacyRunAs(t *testing.T) {
|
||||||
|
|
|
@ -12,7 +12,7 @@ import (
|
||||||
|
|
||||||
"github.com/databricks/cli/bundle/config"
|
"github.com/databricks/cli/bundle/config"
|
||||||
"github.com/databricks/cli/internal/testutil"
|
"github.com/databricks/cli/internal/testutil"
|
||||||
"github.com/databricks/cli/libs/iamutil"
|
"github.com/databricks/cli/libs/auth"
|
||||||
"github.com/databricks/databricks-sdk-go"
|
"github.com/databricks/databricks-sdk-go"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
@ -126,7 +126,7 @@ func TestAccBundleInitHelpers(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
funcName: "{{short_name}}",
|
funcName: "{{short_name}}",
|
||||||
expected: iamutil.GetShortUserName(me),
|
expected: auth.GetShortUserName(me),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
funcName: "{{user_name}}",
|
funcName: "{{user_name}}",
|
||||||
|
@ -138,7 +138,7 @@ func TestAccBundleInitHelpers(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
funcName: "{{is_service_principal}}",
|
funcName: "{{is_service_principal}}",
|
||||||
expected: strconv.FormatBool(iamutil.IsServicePrincipal(me)),
|
expected: strconv.FormatBool(auth.IsServicePrincipal(me.UserName)),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
funcName: "{{smallest_node_type}}",
|
funcName: "{{smallest_node_type}}",
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
package iamutil
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/databricks/databricks-sdk-go/service/iam"
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Determines whether a given user is a service principal.
|
// Determines whether a given user name is a service principal.
|
||||||
// This function uses a heuristic: if the user name is a UUID, then we assume
|
// This function uses a heuristic: if the user name is a UUID, then we assume
|
||||||
// it's a service principal. Unfortunately, the service principal listing API is too
|
// it's a service principal. Unfortunately, the service principal listing API is too
|
||||||
// slow for our purposes. And the "users" and "service principals get" APIs
|
// slow for our purposes. And the "users" and "service principals get" APIs
|
||||||
// only allow access by workspace admins.
|
// only allow access by workspace admins.
|
||||||
func IsServicePrincipal(user *iam.User) bool {
|
func IsServicePrincipal(userName string) bool {
|
||||||
_, err := uuid.Parse(user.UserName)
|
_, err := uuid.Parse(userName)
|
||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
|
@ -1,24 +1,19 @@
|
||||||
package iamutil
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/databricks/databricks-sdk-go/service/iam"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestIsServicePrincipal_ValidUUID(t *testing.T) {
|
func TestIsServicePrincipal_ValidUUID(t *testing.T) {
|
||||||
user := &iam.User{
|
userId := "8b948b2e-d2b5-4b9e-8274-11b596f3b652"
|
||||||
UserName: "8b948b2e-d2b5-4b9e-8274-11b596f3b652",
|
isSP := IsServicePrincipal(userId)
|
||||||
}
|
|
||||||
isSP := IsServicePrincipal(user)
|
|
||||||
assert.True(t, isSP, "Expected user ID to be recognized as a service principal")
|
assert.True(t, isSP, "Expected user ID to be recognized as a service principal")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIsServicePrincipal_InvalidUUID(t *testing.T) {
|
func TestIsServicePrincipal_InvalidUUID(t *testing.T) {
|
||||||
user := &iam.User{
|
userId := "invalid"
|
||||||
UserName: "invalid",
|
isSP := IsServicePrincipal(userId)
|
||||||
}
|
|
||||||
isSP := IsServicePrincipal(user)
|
|
||||||
assert.False(t, isSP, "Expected user ID to not be recognized as a service principal")
|
assert.False(t, isSP, "Expected user ID to not be recognized as a service principal")
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package iamutil
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -12,7 +12,7 @@ import (
|
||||||
// including dots, which are not supported in e.g. experiment names.
|
// including dots, which are not supported in e.g. experiment names.
|
||||||
func GetShortUserName(user *iam.User) string {
|
func GetShortUserName(user *iam.User) string {
|
||||||
name := user.UserName
|
name := user.UserName
|
||||||
if IsServicePrincipal(user) && user.DisplayName != "" {
|
if IsServicePrincipal(user.UserName) && user.DisplayName != "" {
|
||||||
name = user.DisplayName
|
name = user.DisplayName
|
||||||
}
|
}
|
||||||
local, _, _ := strings.Cut(name, "@")
|
local, _, _ := strings.Cut(name, "@")
|
|
@ -1,4 +1,4 @@
|
||||||
package iamutil
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
|
@ -1,7 +1,6 @@
|
||||||
package diag
|
package diag
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/databricks/cli/libs/dyn"
|
"github.com/databricks/cli/libs/dyn"
|
||||||
|
@ -25,9 +24,6 @@ type Diagnostic struct {
|
||||||
// Paths are paths to the values in the configuration tree that the diagnostic is associated with.
|
// Paths are paths to the values in the configuration tree that the diagnostic is associated with.
|
||||||
// It may be nil if there are no associated paths.
|
// It may be nil if there are no associated paths.
|
||||||
Paths []dyn.Path
|
Paths []dyn.Path
|
||||||
|
|
||||||
// A diagnostic ID. Only used for select diagnostic messages.
|
|
||||||
ID ID
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Errorf creates a new error diagnostic.
|
// Errorf creates a new error diagnostic.
|
||||||
|
@ -73,7 +69,7 @@ func Infof(format string, args ...any) Diagnostics {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Diagnostics holds zero or more instances of [Diagnostic].
|
// Diagsnostics holds zero or more instances of [Diagnostic].
|
||||||
type Diagnostics []Diagnostic
|
type Diagnostics []Diagnostic
|
||||||
|
|
||||||
// Append adds a new diagnostic to the end of the list.
|
// Append adds a new diagnostic to the end of the list.
|
||||||
|
@ -100,14 +96,7 @@ func (ds Diagnostics) HasError() bool {
|
||||||
func (ds Diagnostics) Error() error {
|
func (ds Diagnostics) Error() error {
|
||||||
for _, d := range ds {
|
for _, d := range ds {
|
||||||
if d.Severity == Error {
|
if d.Severity == Error {
|
||||||
message := d.Detail
|
return fmt.Errorf(d.Summary)
|
||||||
if message == "" {
|
|
||||||
message = d.Summary
|
|
||||||
}
|
|
||||||
if d.ID != "" {
|
|
||||||
message = string(d.ID) + ": " + message
|
|
||||||
}
|
|
||||||
return errors.New(message)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
package diag
|
|
||||||
|
|
||||||
type ID string
|
|
||||||
|
|
||||||
// For select diagnostic messages we use IDs to identify them
|
|
||||||
// for support or tooling purposes.
|
|
||||||
// It is a non-goal to have an exhaustive list of IDs.
|
|
||||||
const (
|
|
||||||
// We have many subtly different permission errors.
|
|
||||||
// These are numbered for easy reference and tooling support.
|
|
||||||
PathPermissionDenied ID = "EPERM1"
|
|
||||||
ResourcePermissionDenied ID = "EPERM2"
|
|
||||||
CannotChangePathPermissions ID = "EPERM3"
|
|
||||||
RunAsDenied ID = "EPERM4"
|
|
||||||
PermissionNotIncluded ID = "EPERM5"
|
|
||||||
)
|
|
|
@ -103,18 +103,6 @@ func (err CannotDeleteRootError) Is(other error) bool {
|
||||||
return other == fs.ErrInvalid
|
return other == fs.ErrInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
type PermissionError struct {
|
|
||||||
path string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (err PermissionError) Error() string {
|
|
||||||
return fmt.Sprintf("access denied: %s", err.path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (err PermissionError) Is(other error) bool {
|
|
||||||
return other == fs.ErrPermission
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filer is used to access files in a workspace.
|
// Filer is used to access files in a workspace.
|
||||||
// It has implementations for accessing files in WSFS and in DBFS.
|
// It has implementations for accessing files in WSFS and in DBFS.
|
||||||
type Filer interface {
|
type Filer interface {
|
||||||
|
|
|
@ -178,9 +178,6 @@ func (w *workspaceFilesClient) Write(ctx context.Context, name string, reader io
|
||||||
// Create parent directory.
|
// Create parent directory.
|
||||||
err = w.workspaceClient.Workspace.MkdirsByPath(ctx, path.Dir(absPath))
|
err = w.workspaceClient.Workspace.MkdirsByPath(ctx, path.Dir(absPath))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.As(err, &aerr) && aerr.StatusCode == http.StatusForbidden {
|
|
||||||
return PermissionError{absPath}
|
|
||||||
}
|
|
||||||
return fmt.Errorf("unable to mkdir to write file %s: %w", absPath, err)
|
return fmt.Errorf("unable to mkdir to write file %s: %w", absPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -206,11 +203,6 @@ func (w *workspaceFilesClient) Write(ctx context.Context, name string, reader io
|
||||||
return FileAlreadyExistsError{absPath}
|
return FileAlreadyExistsError{absPath}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This API returns StatusForbidden when you have read access but don't have write access to a file
|
|
||||||
if aerr.StatusCode == http.StatusForbidden {
|
|
||||||
return PermissionError{absPath}
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -303,11 +295,11 @@ func (w *workspaceFilesClient) ReadDir(ctx context.Context, name string) ([]fs.D
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: This API returns a 404 if the specified path does not exist,
|
// This API returns a 404 if the specified path does not exist.
|
||||||
// but can also do so if we don't have read access.
|
|
||||||
if aerr.StatusCode == http.StatusNotFound {
|
if aerr.StatusCode == http.StatusNotFound {
|
||||||
return nil, NoSuchDirectoryError{path.Dir(absPath)}
|
return nil, NoSuchDirectoryError{path.Dir(absPath)}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,11 +14,6 @@ type Set[T any] struct {
|
||||||
data map[string]T
|
data map[string]T
|
||||||
}
|
}
|
||||||
|
|
||||||
// Values returns a slice of the set's values
|
|
||||||
func (s *Set[T]) Values() []T {
|
|
||||||
return maps.Values(s.data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSetFromF initialise a new set with initial values and a hash function
|
// NewSetFromF initialise a new set with initial values and a hash function
|
||||||
// to define uniqueness of value
|
// to define uniqueness of value
|
||||||
func NewSetFromF[T any](values []T, f hashFunc[T]) *Set[T] {
|
func NewSetFromF[T any](values []T, f hashFunc[T]) *Set[T] {
|
||||||
|
@ -74,11 +69,6 @@ func (s *Set[T]) Has(item T) bool {
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
// Size returns the number of elements in the set
|
|
||||||
func (s *Set[T]) Size() int {
|
|
||||||
return len(s.data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns an iterable slice of values from set
|
// Returns an iterable slice of values from set
|
||||||
func (s *Set[T]) Iter() []T {
|
func (s *Set[T]) Iter() []T {
|
||||||
return maps.Values(s.data)
|
return maps.Values(s.data)
|
||||||
|
|
|
@ -11,7 +11,7 @@ import (
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
"github.com/databricks/cli/cmd/root"
|
"github.com/databricks/cli/cmd/root"
|
||||||
"github.com/databricks/cli/libs/iamutil"
|
"github.com/databricks/cli/libs/auth"
|
||||||
"github.com/databricks/databricks-sdk-go/apierr"
|
"github.com/databricks/databricks-sdk-go/apierr"
|
||||||
"github.com/databricks/databricks-sdk-go/service/iam"
|
"github.com/databricks/databricks-sdk-go/service/iam"
|
||||||
|
|
||||||
|
@ -119,7 +119,7 @@ func loadHelpers(ctx context.Context) template.FuncMap {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return iamutil.GetShortUserName(cachedUser), nil
|
return auth.GetShortUserName(cachedUser), nil
|
||||||
},
|
},
|
||||||
// Get the default workspace catalog. If there is no default, or if
|
// Get the default workspace catalog. If there is no default, or if
|
||||||
// Unity Catalog is not enabled, return an empty string.
|
// Unity Catalog is not enabled, return an empty string.
|
||||||
|
@ -151,7 +151,7 @@ func loadHelpers(ctx context.Context) template.FuncMap {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result := iamutil.IsServicePrincipal(cachedUser)
|
result := auth.IsServicePrincipal(cachedUser.UserName)
|
||||||
cachedIsServicePrincipal = &result
|
cachedIsServicePrincipal = &result
|
||||||
return result, nil
|
return result, nil
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue