ci / vet-and-test (pull_request) Successful in 42s
Why: GET /api/commands/audit leaked every tenant's audit rows (command ids, entrances, conversation ids, param digests) to any authenticated user; owner was never recorded nor filtered.
Evidence: Red run at a7cc69b FAIL (alice sees bob's row); after fix go test ./... 32/32 packages ok, including TestCommandAuditOwnerScoping and the v18 backfill test (rows recorded on the task card).
Impact: v18 migration adds command_audit.owner with actor-based backfill (empty-actor rows stay invisible via API, kept in-table); AppendAudit/ListAudits gain owner params; cmdbus port and fakes updated; handler passes authenticated owner. No public extension surface change.
Refs: Task card R1; U0 proposal section 5 C7 finding.
78 lines
2.7 KiB
Go
78 lines
2.7 KiB
Go
package store
|
||
|
||
import (
|
||
"path/filepath"
|
||
"testing"
|
||
"time"
|
||
|
||
agentcommand "agent-command"
|
||
)
|
||
|
||
// v18 迁移:command_audit 增加 owner 列并按 payload.actor 回填存量行
|
||
// (GET /api/commands/audit 曾跨租户可读;见 migrate.go v18 注释)。
|
||
// 路径:全新库(已到 v18)→ MigrateDown(17) 回出无 owner 列的旧表 →
|
||
// 直插两行修复前形态的信封 → 重开触发 v18 → 归属断言。
|
||
func TestCommandAuditOwnerBackfill(t *testing.T) {
|
||
path := filepath.Join(t.TempDir(), "audit.db")
|
||
st, err := OpenSQLite(path)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := st.MigrateDown(17); err != nil {
|
||
t.Fatalf("migrate down to v17: %v", err)
|
||
}
|
||
now := time.Now().UTC().Format(time.RFC3339Nano)
|
||
legacy := func(actor, cmd string) string {
|
||
// 修复前形态:信封 Actor 即发起租户,表内无归属列。
|
||
return `{"time":"` + now + `","actor":"` + actor + `","commandId":"` + cmd + `","source":"builtin","state":"committed"}`
|
||
}
|
||
if _, err := st.db.Exec(st.q(`INSERT INTO command_audit (time, command_id, source, entrance, state, operation_id, conversation_id, payload)
|
||
VALUES (?, 'cmd.a', 'builtin', 'api', 'committed', '', 'c1', ?)`), now, legacy("alice", "cmd.a")); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := st.db.Exec(st.q(`INSERT INTO command_audit (time, command_id, source, entrance, state, operation_id, conversation_id, payload)
|
||
VALUES (?, 'cmd.x', 'builtin', 'api', 'committed', '', '', ?)`), now, `{"time":"`+now+`","commandId":"cmd.x","state":"committed"}`); err != nil {
|
||
t.Fatal(err) // actor 为空的存量行:无法归属
|
||
}
|
||
if err := st.Close(); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
st2, err := OpenSQLite(path) // 重开触发 v18:加列 + 回填
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer st2.Close()
|
||
|
||
aliceRows, err := st2.ListAudits("alice", "", 10)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(aliceRows) != 1 || aliceRows[0].CommandID != "cmd.a" {
|
||
t.Fatalf("alice rows = %+v, want 1 条 cmd.a(actor 回填归属)", aliceRows)
|
||
}
|
||
bobRows, err := st2.ListAudits("bob", "", 10)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(bobRows) != 0 {
|
||
t.Fatalf("bob rows = %+v, want 0", bobRows)
|
||
}
|
||
if rows, _ := st2.ListAudits("", "", 10); len(rows) != 0 {
|
||
t.Fatalf("空 owner(actor 缺失的存量行)不得经 API 归属返回,got %d 行", len(rows))
|
||
}
|
||
|
||
// 新写入走 owner 列;同租户按会话过滤仍可用。
|
||
if err := st2.AppendAudit("alice", agentcommand.AuditEnvelope{CommandID: "cmd.a2", Actor: "alice",
|
||
State: agentcommand.StateCommitted, ConversationID: "c2"}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
convRows, err := st2.ListAudits("alice", "c2", 10)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(convRows) != 1 || convRows[0].CommandID != "cmd.a2" {
|
||
t.Fatalf("alice/c2 rows = %+v, want cmd.a2", convRows)
|
||
}
|
||
}
|