Files
Cyrene_v2/internal/channel/session/ownerlane_test.go
T
AskaEth af8591b78a 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 条/分钟
2026-10-04 12:07:40 +08:00

118 lines
4.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
}