- 缺陷:internal/auth/apikey.go 的 key 表是纯内存 map(注释自认 production should persist)⇒ 一次 restart 后所有 key 一律 401;实测挡住了大附件真机 e2e,也与「key 用于生产救火」的长凭据语义矛盾。 - 迁移 0016_api_keys:表 api_keys(id/name/key_hash/created_at/last_used_at/revoked_at)+ 唯一索引 idx_api_keys_key_hash;**只存 SHA-256 hash,无明文入口**。 - 纪律:Generate 先落库后改内存(失败 ⇒ 内存不变 + 返回错误);AttachKeyStore 启动时把库中未吊销 hash 载入内存(Load 失败仍接上 store ⇒ Generate 会响亮报错,而不是静默退回「不落盘」);Revoke/RevokeAll/RevokeByMask 先落库再移出内存(落库失败 ⇒ WARN + 内存不变,fail-closed:绝不让被吊销的 key 重启后复活)。 - 内存表也改为存 hash(不再存明文)⇒ 面板 id/掩码由 hash 派生:不再泄露明文首尾、且跨重启稳定(重启前记下的 id 重启后仍可吊销)。 - 契约零变化:HTTP 请求/响应形状、网关代码、既有内存用例断言全部未动(网关构造注入点只在组合根 wireAPIKeys)。 - 主会话独立验收:隔离 worktree(K1 的 7 文件)build/vet 绿、全树 59 包 0 FAIL、key 用例 24 PASS、双平台交叉编译绿;变异独立复现(跳过落库 ⇒ 重启存活用例红)。 - 取舍与遗留:吊销落库失败在 HTTP 层仍表现为 404/revoked:0(签名零改动的代价,日志可辨);last_used_at 列已建但恒 NULL(避免把读路径变写路径);旧内存 key 已被此前重启消灭,需重签一次后即长期有效。
72 lines
2.5 KiB
Go
72 lines
2.5 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/db"
|
|
)
|
|
|
|
// 本文件锁定组合根接线(K1):wireFrontend 调用的 wireAPIKeys 必须真的把 SQLite
|
|
// 载体接到 key 管理器,否则签发的 key 又回到「重启即失效」。
|
|
//
|
|
// 为什么在组合根测这件事:newApp 无法在单元测试里实例化(要开库、迁移、拉起全部
|
|
// 子系统,见 wire_context_test.go 的说明),所以这里直接对 wireAPIKeys 这一步
|
|
// 接线取证 —— 它与 newApp 里调用的是同一个函数。
|
|
|
|
// openMigratedTestDB 打开一个临时文件库并跑完全部迁移。用文件而非 :memory:,
|
|
// 因为「重启仍在」必须跨连接读同一份数据。
|
|
func openMigratedTestDB(t *testing.T, path string) *sql.DB {
|
|
t.Helper()
|
|
ctx := context.Background()
|
|
conn, err := db.Open(ctx, path, testLog())
|
|
if err != nil {
|
|
t.Fatalf("db.Open(%s): %v", path, err)
|
|
}
|
|
if err := db.MigrateUp(ctx, conn, testLog()); err != nil {
|
|
_ = conn.Close()
|
|
t.Fatalf("db.MigrateUp: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = conn.Close() })
|
|
return conn
|
|
}
|
|
|
|
// TestWireAPIKeys_KeySurvivesRestart 断言组合根接的线真的落盘:签发 → 关连接
|
|
// (进程退出)→ 用同一个库重新接线 → 同一把 key 仍能认证。
|
|
func TestWireAPIKeys_KeySurvivesRestart(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "cyrene.db")
|
|
|
|
first := &app{db: openMigratedTestDB(t, path), cfg: &config.Config{}, log: testLog()}
|
|
keys := first.wireAPIKeys()
|
|
key, err := keys.Generate()
|
|
if err != nil {
|
|
t.Fatalf("Generate: %v", err)
|
|
}
|
|
if err := first.db.Close(); err != nil {
|
|
t.Fatalf("close first connection: %v", err)
|
|
}
|
|
|
|
second := &app{db: openMigratedTestDB(t, path), cfg: &config.Config{}, log: testLog()}
|
|
restarted := second.wireAPIKeys()
|
|
if !restarted.Verify(key) {
|
|
t.Fatal("重启后 key 无法认证:组合根没有把落盘载体接上")
|
|
}
|
|
}
|
|
|
|
// TestWireAPIKeys_NilDBFallsBackToMemory 断言没有数据库句柄时(纯内存测试/
|
|
// 构建路径)接线是空操作且不 panic:key 照旧可签发可校验,只是不跨重启。
|
|
func TestWireAPIKeys_NilDBFallsBackToMemory(t *testing.T) {
|
|
a := &app{db: nil, cfg: &config.Config{}, log: testLog()}
|
|
keys := a.wireAPIKeys() // 必须不 panic
|
|
key, err := keys.Generate()
|
|
if err != nil {
|
|
t.Fatalf("Generate: %v", err)
|
|
}
|
|
if !keys.Verify(key) {
|
|
t.Fatal("未接库路径的 key 无法认证")
|
|
}
|
|
}
|