diff --git a/libs/testserver/dashboards.go b/libs/testserver/dashboards.go index aa7f102849..9e0b1cdbd6 100644 --- a/libs/testserver/dashboards.go +++ b/libs/testserver/dashboards.go @@ -14,6 +14,32 @@ import ( "github.com/databricks/databricks-sdk-go/service/workspace" ) +// workspaceRoots are the top-level directories the real workspace exposes. +var workspaceRoots = []string{"/Users/", "/Repos/", "/Shared/"} + +// isValidWorkspaceParentPath reports whether p is a parent_path cloud accepts: +// an absolute, canonical path (no traversal or control chars) under a workspace +// root. The optional "/Workspace" mount prefix is ignored for the root check. +func isValidWorkspaceParentPath(p string) bool { + if !strings.HasPrefix(p, "/") || p != path.Clean(p) { + return false + } + for _, r := range p { + if r < 0x20 || r == 0x7f { + return false + } + } + if p == "/Workspace" || strings.HasPrefix(p, "/Workspace/") { + p = strings.TrimPrefix(p, "/Workspace") + } + for _, root := range workspaceRoots { + if strings.HasPrefix(p, root) { + return true + } + } + return false +} + // Generate 32 character hex string for dashboard ID func generateDashboardId() (string, error) { randomBytes := make([]byte, 16) @@ -109,6 +135,16 @@ func (s *FakeWorkspace) DashboardCreate(req Request) Response { dashboard.ParentPath = "/Users/" + s.CurrentUser().UserName } + // Reject non-workspace paths like cloud does; storing them verbatim surfaces as drift. + if !isValidWorkspaceParentPath(dashboard.ParentPath) { + return Response{ + StatusCode: 400, + Body: map[string]string{ + "message": fmt.Sprintf("Invalid parent path: %s", dashboard.ParentPath), + }, + } + } + if _, ok := s.directories[dashboard.ParentPath]; !ok { return Response{ StatusCode: 404, diff --git a/libs/testserver/dashboards_test.go b/libs/testserver/dashboards_test.go new file mode 100644 index 0000000000..81d76bcce7 --- /dev/null +++ b/libs/testserver/dashboards_test.go @@ -0,0 +1,40 @@ +package testserver_test + +import ( + "net/http" + "strings" + "testing" + + "github.com/databricks/cli/libs/testserver" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func createDashboard(t *testing.T, baseURL, parentPath string) int { + t.Helper() + body := `{"display_name":"d","parent_path":"` + parentPath + `","warehouse_id":"w"}` + req, _ := http.NewRequest(http.MethodPost, baseURL+"/api/2.0/lakeview/dashboards", strings.NewReader(body)) + req.Header.Set("Authorization", "Bearer test-token") + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + return resp.StatusCode +} + +// A non-workspace parent_path is a 400, even when the directory exists: "/etc/passwd" +// isn't under a workspace root, so it's rejected rather than stored as drift. +func TestDashboardCreateRejectsInvalidParentPath(t *testing.T) { + server := testserver.New(t) + testserver.AddDefaultHandlers(server) + + mkdirs(t, server.URL, "/Users/dev") + mkdirs(t, server.URL, "/Workspace/Users/dev") + assert.Equal(t, 200, createDashboard(t, server.URL, "/Users/dev")) + assert.Equal(t, 200, createDashboard(t, server.URL, "/Workspace/Users/dev")) + + mkdirs(t, server.URL, "/etc/passwd") + assert.Equal(t, 400, createDashboard(t, server.URL, "/etc/passwd")) + assert.Equal(t, 400, createDashboard(t, server.URL, "/Users/../etc")) + assert.Equal(t, 400, createDashboard(t, server.URL, "relative/path")) + assert.Equal(t, 400, createDashboard(t, server.URL, "/Users/\x01dev")) +}