package api import ( "archive/zip" "bytes" "io" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "git.misaka.ren/m1saka/dsh-web/internal/store" ) // Full-home access is an explicit owner capability, not a credential write-only // boundary. Keep ownership, authentication and traversal checks independent. func TestFullHomeOwnerPolicy(t *testing.T) { a, sid := runtimeTestAPI(t, func(w http.ResponseWriter, r *http.Request) { t.Error("unexpected Docker operation") }) for _, i := range []store.Instance{{ID: "owned", UID: "owner", Status: "running"}, {ID: "foreign", UID: "other", Status: "running"}} { if err := a.s.AddInstance(i); err != nil { t.Fatal(err) } home := filepath.Join(a.root, "workspaces", i.UID, "instances", i.ID, "home") if err := os.MkdirAll(filepath.Join(home, "profiles", "web"), 0700); err != nil { t.Fatal(err) } for path, contents := range map[string]string{".credentials.yaml": "synthetic-test-credential", "dsh-ssh.json": "synthetic-test-ssh", "profiles/web/cordis.patch.yml": "old: value"} { if err := os.WriteFile(filepath.Join(home, path), []byte(contents), 0600); err != nil { t.Fatal(err) } } } request := func(method, path, body, session string) *httptest.ResponseRecorder { w := httptest.NewRecorder() a.workspace(w, runtimeRequest(method, path, body, session)) return w } for _, name := range []string{".credentials.yaml", "dsh-ssh.json"} { w := request("GET", "/api/ws/download?instance=owned&path="+name, "", sid) if w.Code != 200 || w.Body.Len() == 0 { t.Fatal(name, w.Code, w.Body.String()) } if w = request("GET", "/api/ws/download?instance=foreign&path="+name, "", sid); w.Code != 404 { t.Fatal("cross-owner read", w.Code) } if w = request("GET", "/api/ws/download?instance=owned&path="+name, "", ""); w.Code != 401 { t.Fatal("anonymous read", w.Code) } } w := request("GET", "/api/ws/download?instance=owned&path=.", "", sid) if w.Code != 200 { t.Fatal(w.Code, w.Body.String()) } archive, err := zip.NewReader(bytes.NewReader(w.Body.Bytes()), int64(w.Body.Len())) if err != nil { t.Fatal(err) } found := false for _, f := range archive.File { if f.Name == ".credentials.yaml" { r, e := f.Open() if e != nil { t.Fatal(e) } data, e := io.ReadAll(r) r.Close() if e != nil || string(data) != "synthetic-test-credential" { t.Fatal("wrong credential archive entry", e) } found = true } } if !found { t.Fatal("owner archive unexpectedly filters credentials") } // Running state intentionally does not force the controlled profile workflow. w = request("POST", "/api/ws/rename?instance=owned", `{"path":"profiles/web/cordis.patch.yml","new_path":"profiles/web/saved.yml"}`, sid) if w.Code != 200 { t.Fatal("owner profile rename blocked", w.Code, w.Body.String()) } w = request("GET", "/api/ws/download?instance=owned&path=../../other/instances/foreign/home/.credentials.yaml", "", sid) if w.Code != 400 && w.Code != 404 { t.Fatal("traversal allowed", w.Code) } }