feat(auth,gateway,user): X-API-Key 真接线 + key 一律超管 + owner 档案真落盘(裁决 15.4.2/15.4.3/10.4.5/08.4.2)
- 15.4.2:管理 API 与 Bearer 共用同一 Authorize 路径,X-API-Key 命中签发表即过门;未知/已吊销/空值 401 且不回落 Bearer(fail-closed) - 15.4.3:key 表去掉角色维度,任一有效 key 鉴权后即 owner;签发接口取消 role 分级(不再读请求体) - 10.4.5:新增 GET/PUT /api/v1/admin/owner-profile(owner-only,PUT 带审计 owner.profile.update),0600 原子落盘 + 活档案热替换;前端主人档案改接真实 API,读不到即禁用保存,删掉硬编码默认档案 - 08.4.2:persona 的 identity.systemPrompt 必填,缺失/空白 400 显式报错,不再派生回落
This commit is contained in:
1 parent
8c6466cb00
commit
adef36538f
13 files changed
+1223
-199
No files matched your search
@@ -1,6 +1,7 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
@@ -156,3 +157,68 @@ func TestRegisterAccount_HotEffect(t *testing.T) {
|
||||
t.Fatalf("accounts after duplicate = %d, want still 2", len(p.Accounts))
|
||||
}
|
||||
}
|
||||
|
||||
// TestOwnerProfile_Replace_HotEffect 钉住后台面板保存后的热替换语义:Replace 把
|
||||
// 新档案装进**同一个指针**(渠道 bridge 解析 actor 用的就是它),因此新白名单
|
||||
// 立即生效、被移除的账号立即失效。换指针的实现会在这里失败。
|
||||
func TestOwnerProfile_Replace_HotEffect(t *testing.T) {
|
||||
t.Parallel()
|
||||
live := &OwnerProfile{ID: "owner", DisplayName: "旧名",
|
||||
Accounts: []OwnerAccount{{Platform: "kook", AccountID: "1"}}}
|
||||
saved := &OwnerProfile{ID: "owner", DisplayName: "叶酱", DefaultPersona: "cyrene",
|
||||
IdentityFile: "configs/users/owner_identity.md", Notes: "主人。",
|
||||
Accounts: []OwnerAccount{{Platform: "telegram", AccountID: "10086"}}}
|
||||
|
||||
live.Replace(saved)
|
||||
|
||||
if live.DisplayName != "叶酱" || live.DefaultPersona != "cyrene" || live.Notes != "主人。" {
|
||||
t.Fatalf("live profile = %+v, want the saved values", live)
|
||||
}
|
||||
if !live.IsOwner("telegram", "10086") {
|
||||
t.Error("newly whitelisted account must be recognized immediately")
|
||||
}
|
||||
if live.IsOwner("kook", "1") {
|
||||
t.Error("removed account must stop being the owner — Replace replaces the whole list")
|
||||
}
|
||||
|
||||
// accounts 是副本:随后改动 saved 不会影响活档案。
|
||||
saved.Accounts[0].AccountID = "999"
|
||||
if live.IsOwner("telegram", "999") || !live.IsOwner("telegram", "10086") {
|
||||
t.Error("live profile must not alias the saved profile's account slice")
|
||||
}
|
||||
}
|
||||
|
||||
// TestOwnerProfile_Replace_NilSafe 断言 nil 入参是安全空操作(不 panic)。
|
||||
func TestOwnerProfile_Replace_NilSafe(t *testing.T) {
|
||||
t.Parallel()
|
||||
live := &OwnerProfile{ID: "owner", DisplayName: "叶酱"}
|
||||
live.Replace(nil)
|
||||
if live.DisplayName != "叶酱" {
|
||||
t.Fatalf("DisplayName = %q, want unchanged", live.DisplayName)
|
||||
}
|
||||
var nilProfile *OwnerProfile
|
||||
nilProfile.Replace(live) // 必须不 panic
|
||||
}
|
||||
|
||||
// TestOwnerProfile_Validate 断言写路径能在落盘前用与载入期同一套规则拒掉坏档案:
|
||||
// 缺 id/displayName、accounts 项只填一半都报错,而空白名单是合法的(全新安装
|
||||
// 就是没人被配成主人,策略引擎默认拒绝)。
|
||||
func TestOwnerProfile_Validate(t *testing.T) {
|
||||
t.Parallel()
|
||||
if err := (&OwnerProfile{ID: "owner", DisplayName: "叶酱"}).Validate(); err != nil {
|
||||
t.Fatalf("valid profile error: %v", err)
|
||||
}
|
||||
if err := (&OwnerProfile{ID: "owner"}).Validate(); !errors.Is(err, ErrRequiredField) {
|
||||
t.Fatalf("missing displayName err = %v, want ErrRequiredField", err)
|
||||
}
|
||||
if err := (&OwnerProfile{DisplayName: "叶酱"}).Validate(); !errors.Is(err, ErrRequiredField) {
|
||||
t.Fatalf("missing id err = %v, want ErrRequiredField", err)
|
||||
}
|
||||
half := &OwnerProfile{ID: "owner", DisplayName: "叶酱", Accounts: []OwnerAccount{{Platform: "kook"}}}
|
||||
if err := half.Validate(); !errors.Is(err, ErrRequiredField) {
|
||||
t.Fatalf("half-filled account err = %v, want ErrRequiredField", err)
|
||||
}
|
||||
if err := (*OwnerProfile)(nil).Validate(); !errors.Is(err, ErrRequiredField) {
|
||||
t.Fatalf("nil profile err = %v, want ErrRequiredField", err)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user