diff --git a/platform/internal/handler/agent_online_acl_test.go b/platform/internal/handler/agent_online_acl_test.go new file mode 100644 index 0000000..afee632 --- /dev/null +++ b/platform/internal/handler/agent_online_acl_test.go @@ -0,0 +1,20 @@ +package handler + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "aijianzhan/platform/internal/authx" +) + +func TestAgentOwnsOnlineDBUsesContextAgent(t *testing.T) { + // 无 Agents store 时必须安全返回 false + r := httptest.NewRequest(http.MethodPost, "/", nil) + ctx := authx.WithFullClaims(context.Background(), 2, 99, authx.Role智能体, 99, 0, []string{authx.Perm数据同步}) + r = r.WithContext(ctx) + if agentOwnsOnlineDB(nil, r, 2, "ch1", "ch1_u3") { + t.Fatal("expected false without svc") + } +} diff --git a/platform/internal/handler/agent_sync.go b/platform/internal/handler/agent_sync.go index a9af4f9..da27194 100644 --- a/platform/internal/handler/agent_sync.go +++ b/platform/internal/handler/agent_sync.go @@ -198,8 +198,8 @@ func authorizeUserSyncPush(svcCtx *svc.ServiceContext, w http.ResponseWriter, r return authorizeUserOnlineDB(svcCtx, w, r, channelID, item.OnlineDBID, "用户自助 push 须带 online_db_id,且须为本人 Binding") } -// authorizeUserOnlineDB:带 online_db_id 时一律按本人 Binding 校验(含有「数据同步」的人类管理员)。 -// 仅「数据同步」且未带 online_db_id 时保持租户级管理路径(智能体/管理员兼容)。 +// authorizeUserOnlineDB:带 online_db_id 时须本人 Binding,或智能体挂载落点匹配(Z14c)。 +// 仅「数据同步」且未带 online_db_id 时保持租户级管理路径。 func authorizeUserOnlineDB(svcCtx *svc.ServiceContext, w http.ResponseWriter, r *http.Request, channelID, onlineDBID, emptyMsg string) bool { online := strings.TrimSpace(onlineDBID) wide := authx.SyncTenantWide(r.Context()) @@ -215,8 +215,41 @@ func authorizeUserOnlineDB(svcCtx *svc.ServiceContext, w http.ResponseWriter, r } uid := authx.UserID(r.Context()) tid := syncTenantID(r) - if !svcCtx.DBSync.Store().UserOwnsOnlineDB(tid, uid, channelID, online) { - authx.WriteError(w, http.StatusForbidden, "无权访问该 online_db_id(非本人 Binding / 非公司共享库)") + if svcCtx.DBSync.Store().UserOwnsOnlineDB(tid, uid, channelID, online) { + return true + } + // Z14c:智能体 JWT 的 user_id=agent_id,Binding 记在人类成员上;放行 agents/me / ticket 挂载的 online_db_id + if agentOwnsOnlineDB(svcCtx, r, tid, channelID, online) { + return true + } + authx.WriteError(w, http.StatusForbidden, "无权访问该 online_db_id(非本人 Binding / 非智能体挂载落点 / 非公司共享库)") + return false +} + +// agentOwnsOnlineDB 当前智能体账号是否已挂载该 online_db_id(及可选 channel)。 +func agentOwnsOnlineDB(svcCtx *svc.ServiceContext, r *http.Request, tenantID int64, channelID, onlineDBID string) bool { + if svcCtx == nil || svcCtx.Agents == nil { + return false + } + aid := authx.AgentID(r.Context()) + if aid <= 0 { + // 兼容旧票:Role=智能体 时 UserID 即 AgentID + if authx.Role(r.Context()) == authx.Role智能体 { + aid = authx.UserID(r.Context()) + } + } + if aid <= 0 || tenantID <= 0 { + return false + } + acc, err := svcCtx.Agents.Get(r.Context(), tenantID, aid) + if err != nil || acc == nil { + return false + } + if strings.TrimSpace(acc.OnlineDBID) != strings.TrimSpace(onlineDBID) { + return false + } + ch := strings.TrimSpace(channelID) + if ch != "" && strings.TrimSpace(acc.ChannelID) != "" && strings.TrimSpace(acc.ChannelID) != ch { return false } return true diff --git a/联调后修改意见-宇恒松离线.md b/联调后修改意见-宇恒松离线.md index 5037199..1b143ef 100644 --- a/联调后修改意见-宇恒松离线.md +++ b/联调后修改意见-宇恒松离线.md @@ -19,7 +19,7 @@ | **Z11** | **智建已落实** | 发布自动补列 | | **Z12** | **双方已接** | 默认同步通道;**默认 Enabled=运行中**(`cb76824`) | | **Z13** | **双方代码已接** | 绑定码/手机/凭票/policy;生产配号+Secret 见 §0.2 B | -| **Z14** | **宇恒已接;智建 fingerprint 已合入待 pull** | 见 §5.11;Z14c `_uN` ACL 仍待智建修 | +| **Z14** | **双方已接;生产 pull** | 指纹 API + Z10d;**Z14c** 智能体挂载 online 已放行;见 §5.11 | | **仍建议关注** | 性能 | SQLite remote 串行 drain;高并发易锁 | ### 0.1 绑定产品冻结摘要(Z13 · 试运行) @@ -49,7 +49,7 @@ | 优先级 | 编号 | 内容 | |--------|------|------| | **P0 配合** | **数据怎么上云** | 见 **§0.3**:绑定 → 库选「同步」→ agent drain;智建「查看线上表」只反映已 push/ensure | -| **P0 配合** | **online_db_id** | 遇 `_uN` **403** 时:暂用本机 `local_database_id` 作可写 id,或省略 online(有「数据同步」管理权时);等 Z14c | +| **P0 配合** | **online_db_id** | 智能体 JWT 可带 `agents/me` / ticket 返回的 `{channel}_uN`(Z14c 已修);人类自助 JWT 仍须本人 Binding | | **P1** | **Z12e / Z7 / §2.6** | env 过渡;Binding 可读名;按库串行 drain | | **P1** | **指纹** | 优先调智建 `schema/fingerprint`;未部署则回退 `schema.row_count` | @@ -70,7 +70,7 @@ | **P0** | 生产再 pull 本批 | Z10d ADD COLUMN + `schema/fingerprint` + 默认启动;`bash ./restart.sh --pull` | | **P0** | 成员手机 | 「宇信达」绑 **`13531041944`**;勿超管 `13531041945` | | **P0** | 通道表白名单 | **勿**「填入测试默认」;形态 B 可空 | -| **P0** | **Z14c** `online_db_id` ACL | ticket/`agents/me` 的 `{channel}_uN` 须与 Binding/JWT 一致,避免 schema/ensure/push **403** | +| **P0** | **Z14c** `online_db_id` ACL | ~~ticket/`agents/me` 的 `{channel}_uN` 403~~ → **已修**:智能体 JWT 放行其挂载的 `online_db_id`(`4fae7ae` 后续) | | **P1** | 凭票 Secret | `YuhengTicket.Secret` ↔ `YXD_YUHENG_TICKET_SECRET` 同值 | | **P1** | `DefaultRemoteDSN` | 生产 Postgres;空则 sqlite 文件(联调) | | **P2** | 正式短信 | Provider≠`"off"` 且 `RequireForBind=true` | @@ -639,7 +639,7 @@ POST .../schema | **Z14a** | **P0** | 宇恒本机指纹 + 对比线上 row_count + 自动修复 | **宇恒已接**:`yxd/app_fastapi/sync_fingerprint.py`;agent 默认 TTL **600s**;`POST /database/sync/fingerprint` | | **Z14b** | **P1** | 智建返回 `content_hash` | **智建已合入待生产 pull**:`POST /api/v1/agent/sync/channels/{id}/schema/fingerprint` | | **Z10d** | **P0** | ensure 对已存在表补列 | **智建已合入待生产 pull**;否则 `填土高度` 等缺 `id` 会 503 | -| **Z14c** | **P1** | ticket/`agents/me` 的 `online_db_id` 与 gateway ACL 一致 | **待智建修**;宇恒暂用本机库 id 或省略 online(见 §0.3) | +| **Z14c** | **P1** | ticket/`agents/me` 的 `online_db_id` 与 gateway ACL 一致 | **智建已落实**:智能体 JWT 校验挂载落点(`agentOwnsOnlineDB`);Binding 仍按人类 user_id | **契约摘要(Z14b)** @@ -656,13 +656,13 @@ POST /api/v1/agent/sync/channels/{id}/schema/fingerprint | 方 | 做什么 | |----|--------| | 宇恒 | 本机算指纹;优先调 fingerprint API;404 则回退 `schema.row_count`;不一致自动 ensure+`enqueue_full_push`;heartbeat 写入 `fingerprint_*` | -| 智建 | 部署 fingerprint + Z10d;修正 `_uN` ACL(Z14c) | +| 智建 | 部署 fingerprint + Z10d;**Z14c 已修**(智能体挂载 online) | **联调踩坑(2026-08-05 · 宇信达生产)** 1. **数据量对不上**:历史行不会因开通同步自动上云 → 须 full-push;指纹对账可检出差表。 2. **`填土高度(6标一工区)`**:本机列 `_row_id`,线上曾无 `id`;push 默认 pk=`id` 并注入该列 → `has no column named id`;整队曾因 `mark_error` 未改 status 卡住(宇恒已修)。修复路径:删表重建 / 部署 Z10d 后 ensure 补 `id` 再推。 -3. **`online_db_id=_u3` 403**:ticket/`agents/me` 返回 `{channel}_u3`,但显式带该 id 调 schema/ensure 被拒;省略或用本机库 id 可写。JWT `user_id=1` 与 `_u3` 后缀不一致,属平台 Binding/ACL 问题。 +3. **`online_db_id=_uN` 403(已修)**:智能体 JWT 的 `user_id=agent_id`,Binding 记在人类成员上;现 `authorizeUserOnlineDB` 额外放行智能体挂载的 `OnlineDBID`/`ChannelID`。 **验收**