- 适配器只描述平台身份: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 条/分钟
118 lines
4.3 KiB
Go
118 lines
4.3 KiB
Go
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)
|
||
}
|
||
}
|