fix(auth): validate stopped workspace restore
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -407,14 +408,35 @@ func verifyRestoreWorkspace(ctx context.Context, installation config.Installatio
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
command := []string{"exec", "-T", "core", "node", "-e"}
|
||||
command := []string{"exec", "-T", "core", "node", "/app/backend/dist/operator-command.js", "workspace-integrity"}
|
||||
if !running {
|
||||
command = []string{"run", "--rm", "--no-deps", "--no-TTY", "core", "node", "-e"}
|
||||
command = []string{"run", "--rm", "--no-deps", "--no-TTY", "core", "node", "/app/backend/dist/operator-command.js", "workspace-integrity"}
|
||||
}
|
||||
command = append(command, `const fs=require("node:fs");const p="/data/workspace-registry/state/active.json";const s=JSON.parse(fs.readFileSync(p,"utf8"));const h=/^[0-9a-f]{40}$/;if(!h.test(s.head)||!Array.isArray(s.revisions)||s.revisions.some(r=>!r||typeof r.id!=="string"||!r.id||!h.test(r.commit)||!h.test(r.blob)))process.exit(1);for(const r of s.revisions)fs.accessSync("/data/workspace-registry/snapshots/"+r.commit+"/"+r.id+".yaml",fs.constants.R_OK)`)
|
||||
result, err := runner.Run(ctx, installation.ComposeArgs(command...), nil)
|
||||
if err != nil {
|
||||
if err != nil || result.ExitCode != 0 {
|
||||
return dockerError("validate restored workspace registry", result, err)
|
||||
}
|
||||
if len(result.Stdout) == 0 || len(result.Stdout) > 4096 {
|
||||
return errors.New("restored workspace registry returned an invalid result")
|
||||
}
|
||||
var payload struct {
|
||||
Ready bool `json:"ready"`
|
||||
State string `json:"state"`
|
||||
Workspaces int `json:"workspaces"`
|
||||
}
|
||||
decoder := json.NewDecoder(strings.NewReader(result.Stdout))
|
||||
decoder.DisallowUnknownFields()
|
||||
if decodeErr := decoder.Decode(&payload); decodeErr != nil {
|
||||
return errors.New("restored workspace registry returned an invalid result")
|
||||
}
|
||||
var trailing any
|
||||
if decodeErr := decoder.Decode(&trailing); !errors.Is(decodeErr, io.EOF) {
|
||||
return errors.New("restored workspace registry returned an invalid result")
|
||||
}
|
||||
if !payload.Ready || payload.Workspaces < 0 ||
|
||||
(payload.State != "active" && payload.State != "uninitialized") ||
|
||||
(payload.State == "uninitialized" && payload.Workspaces != 0) {
|
||||
return errors.New("restored workspace registry did not pass integrity validation")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -511,6 +512,90 @@ func (runner *authenticationStateResetRunner) Stream(context.Context, []string,
|
||||
|
||||
func (*authenticationStateResetRunner) SessionInventoryScope() string { return "mine" }
|
||||
|
||||
type workspaceVerificationRunner struct {
|
||||
running bool
|
||||
result compose.Result
|
||||
err error
|
||||
calls []string
|
||||
}
|
||||
|
||||
func (runner *workspaceVerificationRunner) Run(_ context.Context, args []string, _ io.Reader) (compose.Result, error) {
|
||||
command := strings.Join(args, " ")
|
||||
runner.calls = append(runner.calls, command)
|
||||
if strings.Contains(command, " ps --all --format json") {
|
||||
if runner.running {
|
||||
return compose.Result{Stdout: healthyServicesPayload()}, nil
|
||||
}
|
||||
return compose.Result{}, nil
|
||||
}
|
||||
if strings.Contains(command, "operator-command.js workspace-integrity") {
|
||||
return runner.result, runner.err
|
||||
}
|
||||
return compose.Result{}, fmt.Errorf("unexpected workspace verification command: %s", command)
|
||||
}
|
||||
|
||||
func (*workspaceVerificationRunner) Stream(context.Context, []string, io.Reader, io.Writer) (compose.Result, error) {
|
||||
return compose.Result{}, errors.New("workspace verification must not stream")
|
||||
}
|
||||
|
||||
func (*workspaceVerificationRunner) SessionInventoryScope() string { return "mine" }
|
||||
|
||||
func TestVerifyRestoreWorkspaceUsesFixedNonNetworkOperatorPath(t *testing.T) {
|
||||
installation := preflightTestInstallation(t)
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
running bool
|
||||
payload string
|
||||
prefix string
|
||||
}{
|
||||
{name: "stopped uninitialized", payload: `{"ready":true,"state":"uninitialized","workspaces":0}`, prefix: "run --rm --no-deps --no-TTY core"},
|
||||
{name: "running active", running: true, payload: `{"ready":true,"state":"active","workspaces":1}`, prefix: "exec -T core"},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
runner := &workspaceVerificationRunner{
|
||||
running: test.running,
|
||||
result: compose.Result{Stdout: test.payload},
|
||||
}
|
||||
if err := verifyRestoreWorkspace(context.Background(), installation, runner); err != nil {
|
||||
t.Fatalf("verify restored workspace: %v", err)
|
||||
}
|
||||
if len(runner.calls) != 2 || !strings.Contains(runner.calls[1], test.prefix+" node /app/backend/dist/operator-command.js workspace-integrity") {
|
||||
t.Fatalf("workspace verification calls = %#v", runner.calls)
|
||||
}
|
||||
for _, call := range runner.calls {
|
||||
if strings.Contains(call, "curl") || strings.Contains(call, "-e const") || strings.Contains(strings.ToLower(call), "header") {
|
||||
t.Fatalf("workspace verification used an unsafe command: %s", call)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyRestoreWorkspaceRejectsInvalidOperatorResults(t *testing.T) {
|
||||
installation := preflightTestInstallation(t)
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
result compose.Result
|
||||
err error
|
||||
}{
|
||||
{name: "empty"},
|
||||
{name: "malformed", result: compose.Result{Stdout: `{malformed`}},
|
||||
{name: "trailing document", result: compose.Result{Stdout: `{"ready":true,"state":"active","workspaces":1}{}`}},
|
||||
{name: "unknown field", result: compose.Result{Stdout: `{"ready":true,"state":"active","workspaces":1,"detail":"unsafe"}`}},
|
||||
{name: "not ready", result: compose.Result{Stdout: `{"ready":false,"state":"uninitialized","workspaces":0}`}},
|
||||
{name: "unknown state", result: compose.Result{Stdout: `{"ready":true,"state":"unknown","workspaces":0}`}},
|
||||
{name: "inconsistent count", result: compose.Result{Stdout: `{"ready":true,"state":"uninitialized","workspaces":1}`}},
|
||||
{name: "nonzero", result: compose.Result{ExitCode: 2}, err: errors.New("exit status 2")},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
runner := &workspaceVerificationRunner{result: test.result, err: test.err}
|
||||
if err := verifyRestoreWorkspace(context.Background(), installation, runner); err == nil {
|
||||
t.Fatal("invalid workspace verification result was accepted")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (lock fakeRestoreLock) Release() error {
|
||||
if lock.release != nil {
|
||||
lock.release()
|
||||
|
||||
Reference in New Issue
Block a user