Why: Account disable could be followed by late Backend business writes, API key creation could implicitly create users, account bootstrap locks could invert, and directory cursors were process-local or leaked out-of-scope changes. Evidence: Local go test ./... and isolated PostgreSQL package suites passed on the staged candidate; exact committed-head SELF replay and stage 6 human review remain pending. Impact: Adds final account transactions and external-effect checks across HTTP, Host, Bus and run manager; tightens API key issuance and persists versioned cursor signing. Delegated RDM rename still needs the separately proposed real-operator contract, so A1 and stage 6 are not complete. Refs: Documents U3 r1, approved A1-A4 amendment fb3a821, pending delegated-projection amendment 5783940, Dev feature/iam-u3-account-directory-recovery.
81 lines
3.1 KiB
Go
81 lines
3.1 KiB
Go
package store
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"time"
|
|
|
|
agentcommand "agent-command"
|
|
)
|
|
|
|
// AppendAudit 追加命令审计信封(cmdbus 端口;v7 建表,v18 增加 owner 列)。
|
|
// owner 是租户身份(认证注入),与信封里的 Actor(发起主体)语义不同:查询按
|
|
// owner 过滤,跨租户审计不可见。
|
|
func (s *Store) AppendAudit(owner string, env agentcommand.AuditEnvelope) error {
|
|
raw, err := json.Marshal(env)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = s.db.Exec(s.q(`INSERT INTO command_audit (time, command_id, source, entrance, state, operation_id, conversation_id, owner, payload)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`),
|
|
env.Time.UTC().Format(time.RFC3339Nano), env.CommandID, string(env.Source), env.Entrance, string(env.State),
|
|
env.OperationID, env.ConversationID, owner, string(raw))
|
|
return err
|
|
}
|
|
|
|
// AppendAuditAccountChecked fences command result envelopes against account
|
|
// disable. Security-denial audit uses its separate account-state path.
|
|
func (s *Store) AppendAuditAccountChecked(owner string, env agentcommand.AuditEnvelope, allowImplicitDefault bool) error {
|
|
raw, err := json.Marshal(env)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.withEffectiveAccountWrite(owner, allowImplicitDefault, func(tx *sql.Tx, _ Account) error {
|
|
_, err := tx.Exec(s.q(`INSERT INTO command_audit (time, command_id, source, entrance, state, operation_id, conversation_id, owner, payload)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`),
|
|
env.Time.UTC().Format(time.RFC3339Nano), env.CommandID, string(env.Source), env.Entrance, string(env.State),
|
|
env.OperationID, env.ConversationID, owner, string(raw))
|
|
return err
|
|
})
|
|
}
|
|
|
|
// ListAudits 审计查询(owner 作用域,时间倒序;conversationID 空 = 该租户全局)。
|
|
// owner 为空视为无作用域:不返回任何行(防调用方漏传租户时把未归属存量行
|
|
// 全部泄出——服务端认证层恒注入非空 owner,这里是纵深防御)。
|
|
// v18 回填时 actor 为空的存量行 owner 保持空串,不匹配任何租户:这些行不再经
|
|
// API 返回(数据保留在表内,可直查数据库取证)——见 v18 迁移注释。
|
|
func (s *Store) ListAudits(owner, conversationID string, limit int) ([]agentcommand.AuditEnvelope, error) {
|
|
if owner == "" {
|
|
return []agentcommand.AuditEnvelope{}, nil
|
|
}
|
|
if limit <= 0 || limit > 500 {
|
|
limit = 100
|
|
}
|
|
var (
|
|
rows *sql.Rows
|
|
err error
|
|
)
|
|
if conversationID == "" {
|
|
rows, err = s.db.Query(s.q(`SELECT payload FROM command_audit WHERE owner=? ORDER BY time DESC LIMIT ?`), owner, limit)
|
|
} else {
|
|
rows, err = s.db.Query(s.q(`SELECT payload FROM command_audit WHERE owner=? AND conversation_id=? ORDER BY time DESC LIMIT ?`), owner, conversationID, limit)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
out := []agentcommand.AuditEnvelope{}
|
|
for rows.Next() {
|
|
var raw string
|
|
if err := rows.Scan(&raw); err != nil {
|
|
return nil, err
|
|
}
|
|
var env agentcommand.AuditEnvelope
|
|
if err := json.Unmarshal([]byte(raw), &env); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, env)
|
|
}
|
|
return out, rows.Err()
|
|
}
|