Initial commit: DSH Web Manager
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.misaka.ren/m1saka/dsh-web/internal/docker"
|
||||
"git.misaka.ren/m1saka/dsh-web/internal/store"
|
||||
)
|
||||
|
||||
func previewFrame(stream byte, text string) []byte {
|
||||
header := make([]byte, 8)
|
||||
header[0] = stream
|
||||
binary.BigEndian.PutUint32(header[4:], uint32(len(text)))
|
||||
return append(header, []byte(text)...)
|
||||
}
|
||||
|
||||
func TestProfileDumpFixedCommandAndRedaction(t *testing.T) {
|
||||
daemon := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/v1.47/containers/container/exec":
|
||||
var request struct {
|
||||
Cmd []string
|
||||
AttachStdout, AttachStderr, Tty bool
|
||||
}
|
||||
if json.NewDecoder(r.Body).Decode(&request) != nil {
|
||||
t.Error("invalid exec request")
|
||||
}
|
||||
if r.Method != "POST" || !reflect.DeepEqual(request.Cmd, []string{"dsh", "--profile", "web", "--dump-config"}) || !request.AttachStdout || !request.AttachStderr || request.Tty {
|
||||
t.Errorf("unsafe command: %+v", request)
|
||||
}
|
||||
writeJSON(w, map[string]string{"Id": "exec"})
|
||||
case "/v1.47/exec/exec/start":
|
||||
w.Write(previewFrame(2, "stderr-secret"))
|
||||
w.Write(previewFrame(1, "provider:\n apiKey: direct-secret\n API_KEY: underscore-secret\n password: numeric-secret\n nested:\n - token: nested-secret\n arbitrary: resolved-credential\n endpoint: https://name:[email protected]/?token=secret\n enabled: true\n"))
|
||||
case "/v1.47/exec/exec/json":
|
||||
writeJSON(w, map[string]any{"Running": false, "ExitCode": 0})
|
||||
default:
|
||||
t.Errorf("unexpected Docker call %s", r.URL.Path)
|
||||
w.WriteHeader(404)
|
||||
}
|
||||
}))
|
||||
defer daemon.Close()
|
||||
a := &API{docker: docker.New(daemon.URL)}
|
||||
out, err := a.dumpProfile(context.Background(), "container")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, secret := range []string{"secret", "resolved-credential", "https://"} {
|
||||
if strings.Contains(out, secret) {
|
||||
t.Fatalf("secret leaked: %s", out)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(out, "[REDACTED]") || !strings.Contains(out, "enabled: true") {
|
||||
t.Fatalf("missing structure: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfileDumpPluginSequence(t *testing.T) {
|
||||
daemon := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case strings.HasSuffix(r.URL.Path, "/exec"):
|
||||
writeJSON(w, map[string]string{"Id": "exec"})
|
||||
case strings.HasSuffix(r.URL.Path, "/start"):
|
||||
w.Write(previewFrame(1, "# bundle\n- id: plugin\n disabled: !!js '!ctx.get(\"profileContext\")'\n config:\n apiKey: private-secret\n enabled: true\n"))
|
||||
default:
|
||||
writeJSON(w, map[string]any{"Running": false, "ExitCode": 0})
|
||||
}
|
||||
}))
|
||||
defer daemon.Close()
|
||||
a := &API{docker: docker.New(daemon.URL)}
|
||||
out, err := a.dumpProfile(context.Background(), "container")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(out, "private-secret") || strings.Contains(out, "ctx.get") || !strings.Contains(out, "enabled: true") || !strings.Contains(out, "[REDACTED]") {
|
||||
t.Fatalf("invalid sequence redaction: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfileDumpRejectsUnsafeOutput(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name, output string
|
||||
exit int
|
||||
}{
|
||||
{"invalid", "apiKey: [secret", 0}, {"plain", "secret", 0},
|
||||
{"documents", "a: b\n---\napiKey: secret", 0}, {"failure", "a: b", 1},
|
||||
{"empty", "", 0},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
daemon := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case strings.HasSuffix(r.URL.Path, "/exec"):
|
||||
writeJSON(w, map[string]string{"Id": "exec"})
|
||||
case strings.HasSuffix(r.URL.Path, "/start"):
|
||||
w.Write(previewFrame(1, tc.output))
|
||||
default:
|
||||
writeJSON(w, map[string]any{"Running": false, "ExitCode": tc.exit})
|
||||
}
|
||||
}))
|
||||
defer daemon.Close()
|
||||
a := &API{docker: docker.New(daemon.URL)}
|
||||
if out, err := a.dumpProfile(context.Background(), "container"); err == nil || out != "" {
|
||||
t.Fatalf("unsafe output returned: %q, %v", out, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreviewStreamBounds(t *testing.T) {
|
||||
for _, data := range [][]byte{
|
||||
[]byte("raw secret"), {1, 0}, previewFrame(3, "secret"),
|
||||
previewFrame(1, strings.Repeat("x", previewLimit)),
|
||||
previewFrame(1, "secret")[:10],
|
||||
} {
|
||||
if _, err := previewStdout(bytes.NewReader(data)); err == nil {
|
||||
t.Fatal("accepted invalid stream")
|
||||
}
|
||||
}
|
||||
data := append(previewFrame(2, "secret"), previewFrame(1, "safe")...)
|
||||
got, err := previewStdout(bytes.NewReader(data))
|
||||
if err != nil || string(got) != "safe" {
|
||||
t.Fatalf("stdout: %q %v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
func previewFixture(t *testing.T) (*API, store.Instance, string) {
|
||||
t.Helper()
|
||||
root := t.TempDir()
|
||||
i := store.Instance{ID: "instance", UID: "owner", ContainerID: "container", Status: "stopped"}
|
||||
path := filepath.Join(root, "workspaces", i.UID, "instances", i.ID, "home", "profiles", "web")
|
||||
if err := os.MkdirAll(path, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return &API{root: root}, i, path
|
||||
}
|
||||
|
||||
func TestProfilePreviewManifestAndErrors(t *testing.T) {
|
||||
a, i, path := previewFixture(t)
|
||||
if err := os.WriteFile(filepath.Join(path, "package.json"), []byte(`{"dsh":{"profile":{"bundles":["@deepseek/bundle"],"apiKey":"private-value"}},"scripts":{"secret":"ignored-value"}}`), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
a.profilePreview(w, httptest.NewRequest("GET", "/preview?cmd=cat&path=.credentials.yaml", nil), i)
|
||||
if w.Code != 200 || !strings.Contains(w.Body.String(), "@deepseek/bundle") || strings.Contains(w.Body.String(), "private-value") || strings.Contains(w.Body.String(), "ignored-value") {
|
||||
t.Fatalf("manifest response: %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
if w.Header().Get("Cache-Control") != "no-store" {
|
||||
t.Fatal("preview must not be cached")
|
||||
}
|
||||
daemon := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "daemon-private-value", 500) }))
|
||||
defer daemon.Close()
|
||||
a.docker = docker.New(daemon.URL)
|
||||
i.Status = "running"
|
||||
w = httptest.NewRecorder()
|
||||
a.profilePreview(w, httptest.NewRequest("GET", "/preview", nil), i)
|
||||
if strings.Contains(w.Body.String(), "daemon-private-value") || !strings.Contains(w.Body.String(), "preview_error") {
|
||||
t.Fatalf("unsafe error: %s", w.Body.String())
|
||||
}
|
||||
w = httptest.NewRecorder()
|
||||
a.profilePreview(w, httptest.NewRequest("POST", "/preview", nil), i)
|
||||
if w.Code != 405 {
|
||||
t.Fatalf("method: %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfileBundlesRejectsCredentialSymlink(t *testing.T) {
|
||||
a, i, path := previewFixture(t)
|
||||
credentials := filepath.Join(path, "..", "..", ".credentials.yaml")
|
||||
if err := os.WriteFile(credentials, []byte("private-value"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Symlink(credentials, filepath.Join(path, "package.json")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := a.profileBundles(i); err == nil {
|
||||
t.Fatal("accepted credential symlink")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user