fix(channel,user): owner 车道只认白名单,堵住群主/伪造 role 冒充主人(裁决 06.4.2/06.4.3/06.4.4)

- 适配器只描述平台身份:qq_obv11 的 owner/admin 一律上报 RoleMember,不再上报 RoleOwner
- session 层唯一判主点只认白名单(owner.yaml Accounts ∪ 每渠道 adminIds),nil 档案 fail-closed 拒绝
- 新增「每渠道管理员层」:channels.json ChannelAccount.adminIds[] → runtimeconfig.AdminIDs → OwnerProfile.SetPlatformAdmins(整体替换语义,热重载后移除的 id 立即失效)
- 组合根接线:启动即播种、面板保存先灌白名单再重载适配器(cmd/cyrene/wire_channels.go + 4 条端到端单测)
- 06.4.3 节流与参与度状态按会话独立计数(ConversationKey 三处统一,空会话 GC);06.4.4 未知平台主动发言保守默认 1 条/分钟
This commit is contained in:
AskaEth committed 2026-10-04 12:07:40 +08:00
1 parent 7e7f53bb41
commit af8591b78a
30 files changed
+1289 -144

No files matched your search

+117
View File
@@ -0,0 +1,117 @@
package session
// 本文件覆盖裁决 06.4.2 在会话层的安全反向用例,以及第二处结构化判主来源
// (每渠道账号配置的 adminIds[],channels.json / design/08 §1.1)。
import (
"context"
"testing"
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter"
"git.yeij.top/AskaEth/Cyrene_v2/internal/user"
)
// TestResolveActorOwnerByChannelAdminIDs 覆盖每渠道管理员 id 列表:SetPlatformAdmins
// 写入后命中即进 owner 车道(可多账户),不在列表里的账号——哪怕适配器上报 role=owner
// ——仍是非主人。
func TestResolveActorOwnerByChannelAdminIDs(t *testing.T) {
t.Parallel()
owner := &user.OwnerProfile{ID: "own-1", DisplayName: "Owner"}
owner.SetPlatformAdmins("qq", []string{"admin-1", " admin-2 ", "admin-1", ""})
ids := owner.PlatformAdmins("qq")
if len(ids) != 2 {
t.Fatalf("PlatformAdmins = %v, want 去重并去空白后的两个 id", ids)
}
for _, id := range []string{"admin-1", "admin-2"} {
if actor := ResolveActor(directMsg(id), owner); !actor.IsOwner() {
t.Errorf("adminIds 命中的 %q 未进 owner 车道", id)
}
}
forged := directMsg("group-owner")
forged.Sender = adapter.Actor{Role: adapter.RoleOwner}
if actor := ResolveActor(forged, owner); actor.IsOwner() {
t.Error("不在 adminIds 里的账号被判成了主人")
}
// 整体替换语义:重新声明后旧 id 立刻失效,channels.json 热重载不会留下悬挂管理员。
owner.SetPlatformAdmins("qq", []string{"admin-2"})
if owner.IsOwner("qq", "admin-1") {
t.Error("SetPlatformAdmins 未整体替换,旧管理员 id 仍有效")
}
owner.SetPlatformAdmins("qq", nil)
if owner.IsOwner("qq", "admin-2") || owner.PlatformAdmins("qq") != nil {
t.Error("空列表应清空该平台的管理员层")
}
}
// TestResolveActorNilOwnerIsFailClosed 覆盖没有主人档案的路径:任何入站帧都是非主人
// (fail-closed),适配器上报的 owner 标记也不例外。
func TestResolveActorNilOwnerIsFailClosed(t *testing.T) {
t.Parallel()
msg := directMsg("whoever")
msg.Sender = adapter.Actor{Role: adapter.RoleOwner}
actor := ResolveActor(msg, nil)
if actor.IsOwner() {
t.Fatal("nil 主人档案下仍被判成主人")
}
if actor.Role != adapter.RoleMember {
t.Errorf("Role = %q, want member", actor.Role)
}
}
// TestOwnerFrameMemWritable 走真实 SessionManager 链路断言 MemWritable:伪造 owner 的
// 私聊帧 Owner/MemWritable 全 false(不入主会话上下文、记忆只读);白名单命中的主人私聊
// 为 true;群主(群聊)永远是隔离环境、MemWritable=false。
func TestOwnerFrameMemWritable(t *testing.T) {
t.Parallel()
m, _ := newTestManager(t)
ctx := context.Background()
owner := &user.OwnerProfile{
ID: "own-1",
DisplayName: "Owner",
Accounts: []user.OwnerAccount{{Platform: "qq", AccountID: "o-100"}},
}
forged := directMsg("forged-owner")
forged.Sender = adapter.Actor{Role: adapter.RoleOwner}
if actor := ResolveActor(forged, owner); actor.Role != adapter.RoleMember {
t.Fatalf("伪造 owner 的入站帧 role = %q, want member", actor.Role)
}
forgedSess, err := m.GetOrCreate(ctx, forged, owner)
if err != nil {
t.Fatalf("GetOrCreate(伪造 owner): %v", err)
}
if forgedSess.Owner || forgedSess.MemWritable {
t.Errorf("伪造 owner 帧 Owner=%v MemWritable=%v, want false/false",
forgedSess.Owner, forgedSess.MemWritable)
}
whitelisted := directMsg("o-100")
ownerSess, err := m.GetOrCreate(ctx, whitelisted, owner)
if err != nil {
t.Fatalf("GetOrCreate(白名单): %v", err)
}
if !ownerSess.Owner || !ownerSess.MemWritable {
t.Errorf("白名单主人帧 Owner=%v MemWritable=%v, want true/true",
ownerSess.Owner, ownerSess.MemWritable)
}
groupOwner := groupMsg("g-owner")
groupOwner.Sender = adapter.Actor{Role: adapter.RoleOwner, GroupID: "g1", IsGroup: true}
if actor := ResolveActor(groupOwner, owner); actor.Role != adapter.RoleMember || actor.IsOwner() {
t.Errorf("群主 role=%q owner=%v, want member/false", actor.Role, actor.IsOwner())
}
groupSess, err := m.GetOrCreate(ctx, groupOwner, owner)
if err != nil {
t.Fatalf("GetOrCreate(群主): %v", err)
}
if groupSess.Owner || groupSess.MemWritable || !groupSess.IsGroup {
t.Errorf("群主会话 Owner=%v MemWritable=%v IsGroup=%v, want false/false/true",
groupSess.Owner, groupSess.MemWritable, groupSess.IsGroup)
}
}