feat(auth,gateway): 接线 viewer 只读门 — 12 条监控 GET 对 viewer 开放,敏感档保持 owner/admin
背景:auth.go 注释写着 RoleViewer 是只读监控档,但实现从未接线 —— 管理侧 24 个 GET 全是 owner/admin,viewer 全 403。本批把它接上(手机端「全功能管理」的前置之一)。
- routes.go:新增第四道门 viewerAuth = Authorize(owner, admin, viewer);registerAdminRoutes 传入。**只**把 12 条只读监控 GET 从 writeAuth 换过来:personas / sessions / plugins / plugins-installed / channels / iot / automations / rag / observability{metrics,trace,logs} / notifications;其余 12 条读与 32 条写**一行不动**
- 保持 owner/admin(viewer 403):memories / llm / users / backthink / audit / sessions-export / channels-messages ×2;owner-only(35 条)含凭据获取(login-qr/login-status)、channels view/friends、全部写操作
- 三处「编码旧策略」的既有测试按**新权限矩阵**改写(不是放宽):gateway_test 的 /admin/plugins viewer 403→200 且补一条 memories 403;observability_logs 的 ViewerForbidden→ViewerReadsTail;notifications 的 viewer 403→200
- 新增 rbac_viewer_test.go:5 个顶层 / 185 个子测试(12 条×3 角色 200、9+35 条 viewer 403、admin 在读档非 401/403 且在 owner-only 403、56 条匿名 401、写路由 viewer 403);含 3 次变异验证(含**反向变异**:把 memories 误放开为 viewerAuth 会 FAIL)
- auth.go 的 RoleViewer 注释改为**实际**可见面;Docs/api-client 新增 §2.0 管理侧权限矩阵 + 变更记录 v0.40;顺带修 §0 一处不实陈述(原写「管理端另支持 X-API-Key」→ 标注尚未接线)
- 验收:**隔离 worktree(HEAD + 本片)** go build/vet/test 全绿 + linux/amd64 与 darwin/arm64 交叉编译 + gofmt 干净(主工作树此时被另一代理的在途改动占用,见下条提交)
- 已知遗留(未改前端,按范围只记录):管理面板未按角色隐藏分区 → viewer 打开 memories/users/llm/审计等页会吃 403(应做能力降级)
This commit is contained in:
1 parent
2c100fe3d9
commit
3f79159fbd
7 files changed
+341
-40
No files matched your search
+26
-2
@@ -11,7 +11,7 @@
|
||||
## 0. 基础约定
|
||||
- **Base URL**: `http://<host>:8080`(网关唯一对外端口;生产一般经反代/HTTPS)。
|
||||
- **版本**: REST 全部挂在 `/api/v1/{resource}`;未来破坏性变更升 v2。
|
||||
- **认证**: `Authorization: Bearer <JWT>`(RBAC:`owner`/`admin`/`viewer`);管理端另支持 `X-API-Key`。
|
||||
- **认证**: `Authorization: Bearer <JWT>`(RBAC:`owner`/`admin`/`viewer`,权限矩阵见 §2.0);⚠️ **`X-API-Key` 目前尚未接线**(`internal/auth/apikey.go` 能签发/吊销带角色的 key,但**没有任何鉴权路径接受它**——当前只有 Bearer JWT 生效;接线属后续批次,接线前请勿依赖该头)。
|
||||
- **响应统一**: `{"data": ...}` / `{"error": {"code": <string>, "message": <string>}}`;状态码 200/201/204/400/401/403/404/429/500。
|
||||
- **错误码**:业务错误带 `code` 字符串(如 `unauthorized` / `not_found`),便于客户端分支。
|
||||
|
||||
@@ -114,7 +114,30 @@
|
||||
|
||||
## 2. REST 端点(管理,`/api/v1/admin/*`)
|
||||
|
||||
> 仅供管理面板与授权客户端;RBAC:`owner`/`admin`/`viewer`(viewer 只读、写 403);**写操作全程审计**。
|
||||
> 仅供管理面板与授权客户端;RBAC:`owner`/`admin`/`viewer`;**写操作全程审计**。角色分档见下方「管理侧权限矩阵」(v0.40 起 `viewer` 真正可用:只读监控档,其余一律 `403`)。
|
||||
|
||||
### 2.0 管理侧权限矩阵(v0.40 起,与 `internal/services/gateway/routes.go` 逐条一致)
|
||||
|
||||
- **匿名(无 token / token 无效)**:任意 `/api/v1/admin/*` → `401 {error:{code:"unauthorized"}}`(fail-closed,全部 56 条管理路由无一例外)。
|
||||
- **角色分档**(`✅` 放行,`❌` 回 `403 forbidden`;owner>admin>viewer 递增):
|
||||
|
||||
| 端点组 | owner | admin | viewer |
|
||||
|------|:---:|:---:|:---:|
|
||||
| **只读监控档**(12 条 GET,见下①) | ✅ | ✅ | ✅ |
|
||||
| **owner/admin 只读档**(9 条,见下②) | ✅ | ✅ | ❌ |
|
||||
| **owner-only 写/凭据档**(35 条,见下③) | ✅ | ❌ | ❌ |
|
||||
|
||||
① **`viewer` 可读的 12 条只读监控档**(`viewerAuth` = owner/admin/viewer)——模块清单 + 运行时指标/日志/通知,全部是**非敏感投影**(无凭据、无他人账号数据、无消息正文):
|
||||
`GET /admin/personas`、`GET /admin/sessions`、`GET /admin/plugins`、`GET /admin/plugins/installed`、`GET /admin/channels`、`GET /admin/iot`、`GET /admin/automations`、`GET /admin/rag`、`GET /admin/observability/metrics`、`GET /admin/observability/trace`、`GET /admin/observability/logs`、`GET /admin/notifications`。
|
||||
|
||||
② **仅 `owner`/`admin` 的 9 条**(`viewer` → `403`;理由:含**他人数据 / 隐私正文 / 完整导出 / 审计流水 / 全局凭据面**):
|
||||
`GET /admin/memories`(私人记忆正文)、`GET /admin/channels/messages`、`GET /admin/channels/{platform}/messages`(完整 IM 消息流/转录)、`GET /admin/llm`(provider/模型配置面)、`GET /admin/users`(账号与 API key 列表)、`GET /admin/backthink`(后台思考记录)、`GET /admin/observability/audit`(审计流水)、`GET /admin/sessions/{id}/export`(会话导出)、`POST /admin/rag/query`(知识库检索,语义上是读,但按「全局知识面」归此档)。
|
||||
|
||||
③ **仅 `owner` 的 35 条**(`admin` 与 `viewer` 均 `403`)——全部写操作,外加**凭据获取路径**(`login-qr`/`login-status`,`view`/`friends`,渠道连接/测试/配置):
|
||||
`GET /admin/channels/{platform}/login-qr`、`login-status`、`view`、`friends`;`POST /admin/channels/{platform}/friend/handle`、`message/test`;`PUT|DELETE /admin/channels/{platform}/config`;`POST /admin/channels/open|close|test`;`POST /admin/plugins/policy|install|uninstall`;`POST /admin/personas`、`PUT /admin/personas/{id}`;`POST /admin/users`、`POST /admin/users/apikey`、`DELETE /admin/users/apikey/{id}`;`POST /admin/iot/{id}/control`、`POST /admin/iot/scenarios`、`POST /admin/iot/rules`;`POST /admin/automations`、`POST /admin/automations/{id}/run`、`DELETE /admin/automations/{id}`;`POST /admin/llm`、`POST /admin/llm/test`;`POST /admin/memories`、`POST /admin/memories/search`;`POST /admin/rag`、`POST /admin/rag/delete`;`POST /admin/system/revoke-keys|rotate-jwt|backup|restore`。
|
||||
|
||||
> **用户侧同级只读**(非 admin 前缀,同样任意已认证角色可读):`GET /api/v1/sessions`、`GET /api/v1/personas`、`GET /api/v1/meta/enums`(§1 行 4/9/15)。
|
||||
> **已知遗留**:前端管理面板**尚未**按角色隐藏/禁用分区——`viewer` 打开②③档页面会拿到 `403`,客户端需按本矩阵自行降级(后端契约已定,前端适配后置)。
|
||||
|
||||
| # | 端点 | 方法 | 状态 | 说明 |
|
||||
|---|------|------|:---:|------|
|
||||
@@ -329,6 +352,7 @@
|
||||
- 语音:上行 `voice_input`(base64 音频或已转写 text),下行 `voice_output`(合成音频 base64 + text)。
|
||||
|
||||
## 5. 变更记录
|
||||
- `v0.40`(**`viewer` 角色真正接线:只读监控档**,对客户端**收紧/放宽若干 admin 读的鉴权**,无端点/字段形状变更):此前 `viewer` 只是常量(`internal/auth` 的 `RoleViewer` 注释写着「只读 dashboard/logs/monitoring」)而**从未接线**——管理侧 24 条 GET 全走 owner/admin,`viewer` 一条都读不到(**全 403**)。本版新增第四道门 `viewerAuth = owner+admin+viewer`,并把 **12 条只读监控 GET** 从 owner/admin 放宽到 owner/admin/**viewer**:`GET /admin/personas`、`/admin/sessions`、`/admin/plugins`、`/admin/plugins/installed`、`/admin/channels`、`/admin/iot`、`/admin/automations`、`/admin/rag`、`/admin/observability/metrics`、`/admin/observability/trace`、`/admin/observability/logs`、`/admin/notifications`。**明确不放给 viewer**(含凭据获取路径、他人数据、完整消息/导出明文、审计流水,或 `admin` 也不给):`GET /admin/memories`、`/admin/llm`、`/admin/users`、`/admin/backthink`、`/admin/observability/audit`、`/admin/channels/messages`、`/admin/channels/{platform}/messages`、`/admin/sessions/{id}/export`、`POST /admin/rag/query`(这 9 条 owner/admin)以及其余 **35 条 owner-only**(全部写操作 + `login-qr`/`login-status`/`view`/`friends`/渠道连接与配置/`plugins/*`/`users/*`/`system/*`)。**匿名**:任意 admin 路由仍 `401`(fail-closed)。**向后兼容**:`owner`/`admin` 语义不变(放行集合是超集),仅 `viewer` 的可见面从「全 403」变为 12 条只读监控;不依赖 `viewer` 的客户端行为不变。完整分档见 §2.0「管理侧权限矩阵」。
|
||||
- `v0.39`(**微信 iLink 会话被拒可见化 + 不再假 OK**,对客户端**新增两个可选字段、一处错误码**,向后兼容):① **`GET /api/v1/admin/channels` 每平台行新增可选 `needsRescan`(bool,`omitempty`)+ `lastError`(string,`omitempty`)**——仅当适配器**真的**要求重新授权时出现(今天只有微信 iLink 返回 `errcode=-14` 这一种),取值来自适配器新增的**可选能力接口** `RescanRequired() (bool, string)`,网关**经类型断言**读取(主 `PlatformAdapter` 契约**不变**;未实现该能力的平台字段**缺省不出现**)。用途:把「需要重新扫码」与「主人自己关掉的未连接」区分开——后者仅 `connected:false`。② **`POST /admin/channels/open`** 对「iLink 已拒绝该会话」回 **`409 session_rejected`**(message 明确要求用「扫码登录」重新绑定),**不再**吞掉错误回 200;`409 needs_login`(尚无 token,v0.8 起)不变。③ **适配器行为**:iLink `errcode=-14` 进入**显式被拒态**——`IsConnected()` 恒 `false`;`Connect()` 返回被拒错误(**不再**对被拒会话报成功);`HealthCheck` 区分「会话被拒」与「未连接」;并以 **`ERROR`** 级日志写明「本次会话已被 iLink 拒绝,需要重新扫码绑定」(只带 `bot_id` 等非敏感标识,**token 永不入日志**)。被拒态在读循环与发送两条路径**都会**置位,并仅在**扫码登录拿到新 token** 或**配置换了新 token**(保存 + 热重载)时清除。④ 扫码登录侧:被拒期间 `login-status` 报 **`failed` + 被拒原因**(**不再**假报 `confirmed`,否则面板会把已作废凭据当成功重新落盘);`login-qr` 仍可正常出码(**不**回 `already_logged_in`,否则重新扫码这条路被堵死);`get_bot_qrcode` 不再携带已被拒的 token(否则服务端以「已绑定」应答、**不**发新凭据)。**向后兼容**:只新增可选字段与一处更诚实的错误码;不读新字段、不处理 409 的客户端行为不变(此时面板显示「未连接」,仍可用「扫码登录」恢复)。**语义澄清(本次仅措辞,不改契约形状、不另起版本号——v0.39 尚未被任何客户端采用)**:iLink 的 bot token 是**长期凭据**(实测一个月前扫码得到的 token 仍可用),`errcode=-14` 表示 **iLink 侧拒绝/作废了该会话凭据**,**不是时间性过期**。详见 §2 行 6 与「微信 iLink『会话被拒』契约」。
|
||||
- `v0.38`(**B4 客户端共享枚举权威表 + 未知值回落约定**,对客户端**新增一个只读 REST 端点**):新增 **`GET /api/v1/meta/enums`**(**任意已认证角色可读**,与 `GET /sessions` 同级;无 token `401`)→ `200 {data:{platforms,moods,modes,unknown}}`,每项 `{id,label}`:① **`platforms`**(6)= `web`/`kook`/`qq-obv11`/`weixin-ilink`/`telegram`/`webhook`——即各内置适配器 `Name()` + Web 主会话来源 `web`;② **`moods`**(5)= `calm`/`happy`/`excited`/`sad`/`angry`,与 persona 情绪状态机内置状态一致(该状态机**可扩展**,故非封闭集合);③ **`modes`**(9)= `chat`/`code`/`home`/`work`/`docs`/`voice`/`vision`/`agents`/`settings`(design/07 §1);④ **`unknown`** 恒 `"display-raw"` = **未知值回落约定机器可读**:客户端遇不在表中的取值**原样显示原始字符串**(**不丢弃、不报错、不占位符**),**服务端新增取值视为非破坏性变更**(forward-compatible)。**单一事实来源在 Go 侧**(`internal/contract/enums`),并有**反漂移测试**断言适配器 `Name()` / 情绪内置状态 / 前端九模式与三张表一致,漏登记即 CI FAIL。Web 侧镜像 `frontend/web/src/lib/enums.ts`(`labelFor` 未知值返回原值),顶栏/侧栏/消息气泡的「心情」标签已改用它渲染。**向后兼容**:仅新增端点,既有契约形状不变。详见 §1 行 15 与 §1.1。
|
||||
- `v0.37`(**D4 未读状态 / 已读游标**,对客户端**新增一个语义字段、一个 REST 端点、一个下行事件**):① `chat_sessions` 新增 **`read_at INTEGER NOT NULL DEFAULT 0`**(迁移 **0010**,down 可逆;**毫秒 Unix 时间戳**,与 `chat_messages.created_at` **同单位**)。② **语义**:**会话级单一已读游标 + 单 owner 账号**——同一账号所有端共用一个游标,**读一处 = 全端已读**(不做 per-device/per-user 游标,与「同一账号所有浏览器都属于同一 owner」一致);`createdAt <= readAt` 的消息算已读,未读数 = `createdAt > readAt` 的条数。③ **`GET /api/v1/sessions` 每项新增恒返回的 `unreadCount`**(整数,0 = 无未读):与 `lastMessage` **同一条 SQL 派生**(关联子查询 `COUNT(*) WHERE session_id = s.id AND created_at > s.read_at`,走已有 `idx_chat_msgs_session_created`,**无 N+1、不新增索引**)。④ **新增 `POST /api/v1/sessions/{id}/read`**(owner/admin,与 `PATCH /sessions/{id}` 同鉴权):body `{before?}`(可选毫秒;缺省= 现在)→ `200 {data:{sessionId,readAt}}`;`before` 为 0/负数/非数字/body 非对象 → `400`,未知会话(含隔离 `sess-xxx`)→ `404`,未注入 HistoryStore → `501`。**游标只前进不后退**(`read_at = MAX(read_at, before)`),乱序/并发/重试请求幂等安全。⑤ **新增下行事件 `session_read`**:`{type:"session_read", sessionId, data:{readAt}}`,全连接广播 + 客户端按 `sessionId` 过滤,用于同账号其它端清红点;失败路径不广播(§3.9)。⑥ **升级回填**:0010 把既有会话游标回填为「该会话现有最新消息时间」,**升级当天不误报未读**(既有历史全部视为已读)。⑦ `HistoryStore`(网关消费侧契约)新增 `MarkSessionRead(ctx, sessionID, before)`;`SessionMeta` 新增 `UnreadCount`。**向后兼容**:仅新增字段/端点/事件,既有契约形状不变;不消费 `unreadCount`/`session_read` 的客户端行为不变。详见 §1 行 4/8a 与 §3.9。
|
||||
|
||||
Reference in new issue
Block a user