Add a red test for cross-tenant command audit leakage

Why: GET /api/commands/audit returns every tenant's audit rows; the regression test pins the desired owner-scoped behavior before the fix.
Evidence: go test ./internal/server -run TestCommandAuditOwnerScoping FAIL (owner alice sees bob's row; output recorded on the task card).
Impact: Test only; suite red on this commit by design.
Refs: Task card R1; U0 proposal section 5 C7 security finding.
This commit is contained in:
2026-10-08 13:57:30 +00:00
parent fe7c245f5c
commit a7cc69b61e
+68
View File
@@ -0,0 +1,68 @@
package server
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
agentcommand "agent-command"
)
// 命令审计按租户作用域(回归:GET /api/commands/audit 曾返回全租户审计行——
// ListAudits 无 owner 条件、command_audit 无 owner 列;多用户档下任何登录
// 用户可读他人命令 ID、入口、会话 ID 与参数摘要)。
// 经真实认证中间件(API key → owner)请求,断言每个租户只见本租户的行。
func TestCommandAuditOwnerScoping(t *testing.T) {
srv, st := newAuthTestServer(t)
h := srv.Handler()
aliceKey, err := st.CreateAPIKey("alice", "e2e")
if err != nil {
t.Fatal(err)
}
bobKey, err := st.CreateAPIKey("bob", "e2e")
if err != nil {
t.Fatal(err)
}
// 两租户各产生一条命令审计(信封 Actor=发起租户——修复前唯一可用的归属线索)。
if err := st.AppendAudit(agentcommand.AuditEnvelope{
CommandID: "cmd.alice", Actor: "alice", State: agentcommand.StateCommitted,
}); err != nil {
t.Fatal(err)
}
if err := st.AppendAudit(agentcommand.AuditEnvelope{
CommandID: "cmd.bob", Actor: "bob", State: agentcommand.StateCommitted,
}); err != nil {
t.Fatal(err)
}
get := func(key string) []map[string]any {
t.Helper()
req := httptest.NewRequest(http.MethodGet, "/api/commands/audit", nil)
req.Header.Set("Authorization", "Bearer "+key)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("audit query: %d %s", rec.Code, rec.Body.String())
}
var out struct {
Audits []map[string]any `json:"audits"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &out); err != nil {
t.Fatal(err)
}
return out.Audits
}
for key, owner := range map[string]string{aliceKey: "alice", bobKey: "bob"} {
rows := get(key)
if len(rows) != 1 {
t.Fatalf("owner %s: audit rows = %d, want 1(跨租户行不得返回;got=%v)", owner, len(rows), rows)
}
if rows[0]["actor"] != owner || rows[0]["commandId"] != "cmd."+owner {
t.Fatalf("owner %s: wrong row %v", owner, rows[0])
}
}
}