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:
1 parent
7e7f53bb41
commit
af8591b78a
30 files changed
+1289
-144
No files matched your search
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user