缺口①:「连接」是按平台连、且恒打第一条账号(channelActionRequest 只有 name)⇒ 主人有两条
qq-obv11 时点到的是 mode=server 的历史条目,报 server mode requires listenAddr,而正确的
client 条目轮不到。缺口②:空 id 条目既删不掉(DELETE 要求 id 非空)也改不了(UpsertChannel
空 id = 新建),越改越多。
后端:
- POST /admin/channels/open|close 请求体新增可选 accountId(缺省逐字兼容,老客户端零影响);
按该账号驱动;未知 id ⇒ 404 且零连接(不静默回落第一条);已配置未注册 ⇒ 409
- DELETE /admin/channels/{platform}/config 新增显式 empty=1(id 与 empty=1 互斥 ⇒ 400;
无可删 ⇒ 404);语义严格 —— 只删空 id 条目,绝不把「id 为空」默认解释成删空 id
- 响应新增:open/close 的 resolved(实际驱动的适配器名)与 accountId;delete 的 removed/empty;
GET /admin/channels 的 accounts[].connected(可选 bool,缺省 = 未知)
- 审计动作名不变(channel.open/close/delete),日志行新增 accountId 字段
前端:每条账号一行「连接/断开」(带 accountId);按钮旁明确「会连哪一条」;空 id 行显著标注
+ 删除入口;channelActionBody 携带 accountId;新增 action 测试 33 checks + 空 id 行渲染 31 checks。
测试:后端 14 个用例(真实 HTTP 路由 + JWT + 真注册表 + spy 适配器,断言「哪一条被驱动、
另一条一次都没被碰」)。
变异 6 条(accountId 被忽略 / id 为空当删空 id / 找不到静默回落 / 前端丢 accountId /
删空 id 退化成 ?id= / 空 id 行按普通行渲染)。
65 lines
2.6 KiB
Go
65 lines
2.6 KiB
Go
package gateway
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"time"
|
|
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/auth"
|
|
)
|
|
|
|
// auditClaims 记录一次管理写操作,调用方身份从请求上下文读取
|
|
// (由 auth.Authorize 填充)。它是管理操作所需的
|
|
// 谁/什么/何时/结果审计链;「主代理代审」这一面向用户的提示是
|
|
// 前端的职责,不是网关的。
|
|
func (s *Server) auditClaims(r *http.Request, what, result string) {
|
|
s.auditClaimsAccount(r, what, result, "")
|
|
}
|
|
|
|
// auditClaimsAccount 在既有审计口径(what/result 不变,便于按动作聚合)之上补一个
|
|
// 账号维度,使多账号平台上的「连/断/删的是哪一条」在审计里可追溯。accountID 为空
|
|
// (平台级动作)时不带该字段。附加字段只放账号 id —— 凭据永不进审计/日志。
|
|
func (s *Server) auditClaimsAccount(r *http.Request, what, result, accountID string) {
|
|
who := "anonymous"
|
|
if claims, ok := auth.ClaimsFromContext(r.Context()); ok {
|
|
who = claims.Sub + ":" + string(claims.Role)
|
|
}
|
|
extra := []any{}
|
|
if accountID != "" {
|
|
extra = append(extra, "accountId", accountID)
|
|
}
|
|
s.audit(r.Context(), who, what, result, extra...)
|
|
}
|
|
|
|
// AuditEvent 记录一次带显式调用方身份的外部(非 HTTP)审计事件,
|
|
// 使请求路径之外的程序化写入方(例如渠道 bridge 的
|
|
// 黑名单拒绝钩子,design/16 §4.6 §6)能追加到同一
|
|
// 谁/什么/何时/结果链。who 是自由形式的 actor 标签("channel" 或
|
|
// "bridge:telegram");what 是操作,result 是其结果。它对
|
|
// 并发使用安全。
|
|
func (s *Server) AuditEvent(who, what, result string) {
|
|
s.audit(context.Background(), who, what, result)
|
|
}
|
|
|
|
// audit 在网关 logger 上发出一条结构化审计记录,并把它持久化到
|
|
// 已配置的 AuditStore(未注入时回退到内存存储)。
|
|
// 持久化失败会记日志,但绝不阻塞管理写,因此
|
|
// 坏掉的审计存储无法拖垮审计链的首要职责。
|
|
// extra 是追加的结构化字段(键值交替,如 "accountId", "abc"),
|
|
// 只用于日志行;持久化记录的口径保持 who/what/result 不变。
|
|
func (s *Server) audit(ctx context.Context, who, what, result string, extra ...any) {
|
|
fields := []any{
|
|
"who", who,
|
|
"what", what,
|
|
"when", time.Now().UTC().Format(time.RFC3339),
|
|
"result", result,
|
|
}
|
|
fields = append(fields, extra...)
|
|
s.log.Info("admin audit", fields...)
|
|
if s.obs != nil && s.obs.audit != nil {
|
|
if err := s.obs.audit.Append(ctx, who, what, result, time.Now()); err != nil {
|
|
s.log.Error("admin audit persist", "err", err, "what", what)
|
|
}
|
|
}
|
|
}
|