fix: allow agent JWT to use attached online_db_id (Z14c)
Agent tokens use agent_id as user_id while Binding is under the human member; authorize schema/push/ensure against the agent's mounted OnlineDBID. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
20
platform/internal/handler/agent_online_acl_test.go
Normal file
20
platform/internal/handler/agent_online_acl_test.go
Normal file
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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")
|
return authorizeUserOnlineDB(svcCtx, w, r, channelID, item.OnlineDBID, "用户自助 push 须带 online_db_id,且须为本人 Binding")
|
||||||
}
|
}
|
||||||
|
|
||||||
// authorizeUserOnlineDB:带 online_db_id 时一律按本人 Binding 校验(含有「数据同步」的人类管理员)。
|
// authorizeUserOnlineDB:带 online_db_id 时须本人 Binding,或智能体挂载落点匹配(Z14c)。
|
||||||
// 仅「数据同步」且未带 online_db_id 时保持租户级管理路径(智能体/管理员兼容)。
|
// 仅「数据同步」且未带 online_db_id 时保持租户级管理路径。
|
||||||
func authorizeUserOnlineDB(svcCtx *svc.ServiceContext, w http.ResponseWriter, r *http.Request, channelID, onlineDBID, emptyMsg string) bool {
|
func authorizeUserOnlineDB(svcCtx *svc.ServiceContext, w http.ResponseWriter, r *http.Request, channelID, onlineDBID, emptyMsg string) bool {
|
||||||
online := strings.TrimSpace(onlineDBID)
|
online := strings.TrimSpace(onlineDBID)
|
||||||
wide := authx.SyncTenantWide(r.Context())
|
wide := authx.SyncTenantWide(r.Context())
|
||||||
@@ -215,8 +215,41 @@ func authorizeUserOnlineDB(svcCtx *svc.ServiceContext, w http.ResponseWriter, r
|
|||||||
}
|
}
|
||||||
uid := authx.UserID(r.Context())
|
uid := authx.UserID(r.Context())
|
||||||
tid := syncTenantID(r)
|
tid := syncTenantID(r)
|
||||||
if !svcCtx.DBSync.Store().UserOwnsOnlineDB(tid, uid, channelID, online) {
|
if svcCtx.DBSync.Store().UserOwnsOnlineDB(tid, uid, channelID, online) {
|
||||||
authx.WriteError(w, http.StatusForbidden, "无权访问该 online_db_id(非本人 Binding / 非公司共享库)")
|
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 false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
| **Z11** | **智建已落实** | 发布自动补列 |
|
| **Z11** | **智建已落实** | 发布自动补列 |
|
||||||
| **Z12** | **双方已接** | 默认同步通道;**默认 Enabled=运行中**(`cb76824`) |
|
| **Z12** | **双方已接** | 默认同步通道;**默认 Enabled=运行中**(`cb76824`) |
|
||||||
| **Z13** | **双方代码已接** | 绑定码/手机/凭票/policy;生产配号+Secret 见 §0.2 B |
|
| **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;高并发易锁 |
|
| **仍建议关注** | 性能 | SQLite remote 串行 drain;高并发易锁 |
|
||||||
|
|
||||||
### 0.1 绑定产品冻结摘要(Z13 · 试运行)
|
### 0.1 绑定产品冻结摘要(Z13 · 试运行)
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
| 优先级 | 编号 | 内容 |
|
| 优先级 | 编号 | 内容 |
|
||||||
|--------|------|------|
|
|--------|------|------|
|
||||||
| **P0 配合** | **数据怎么上云** | 见 **§0.3**:绑定 → 库选「同步」→ agent drain;智建「查看线上表」只反映已 push/ensure |
|
| **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** | **Z12e / Z7 / §2.6** | env 过渡;Binding 可读名;按库串行 drain |
|
||||||
| **P1** | **指纹** | 优先调智建 `schema/fingerprint`;未部署则回退 `schema.row_count` |
|
| **P1** | **指纹** | 优先调智建 `schema/fingerprint`;未部署则回退 `schema.row_count` |
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
| **P0** | 生产再 pull 本批 | Z10d ADD COLUMN + `schema/fingerprint` + 默认启动;`bash ./restart.sh --pull` |
|
| **P0** | 生产再 pull 本批 | Z10d ADD COLUMN + `schema/fingerprint` + 默认启动;`bash ./restart.sh --pull` |
|
||||||
| **P0** | 成员手机 | 「宇信达」绑 **`13531041944`**;勿超管 `13531041945` |
|
| **P0** | 成员手机 | 「宇信达」绑 **`13531041944`**;勿超管 `13531041945` |
|
||||||
| **P0** | 通道表白名单 | **勿**「填入测试默认」;形态 B 可空 |
|
| **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** | 凭票 Secret | `YuhengTicket.Secret` ↔ `YXD_YUHENG_TICKET_SECRET` 同值 |
|
||||||
| **P1** | `DefaultRemoteDSN` | 生产 Postgres;空则 sqlite 文件(联调) |
|
| **P1** | `DefaultRemoteDSN` | 生产 Postgres;空则 sqlite 文件(联调) |
|
||||||
| **P2** | 正式短信 | Provider≠`"off"` 且 `RequireForBind=true` |
|
| **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` |
|
| **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` |
|
| **Z14b** | **P1** | 智建返回 `content_hash` | **智建已合入待生产 pull**:`POST /api/v1/agent/sync/channels/{id}/schema/fingerprint` |
|
||||||
| **Z10d** | **P0** | ensure 对已存在表补列 | **智建已合入待生产 pull**;否则 `填土高度` 等缺 `id` 会 503 |
|
| **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)**
|
**契约摘要(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 API;404 则回退 `schema.row_count`;不一致自动 ensure+`enqueue_full_push`;heartbeat 写入 `fingerprint_*` |
|
||||||
| 智建 | 部署 fingerprint + Z10d;修正 `_uN` ACL(Z14c) |
|
| 智建 | 部署 fingerprint + Z10d;**Z14c 已修**(智能体挂载 online) |
|
||||||
|
|
||||||
**联调踩坑(2026-08-05 · 宇信达生产)**
|
**联调踩坑(2026-08-05 · 宇信达生产)**
|
||||||
|
|
||||||
1. **数据量对不上**:历史行不会因开通同步自动上云 → 须 full-push;指纹对账可检出差表。
|
1. **数据量对不上**:历史行不会因开通同步自动上云 → 须 full-push;指纹对账可检出差表。
|
||||||
2. **`填土高度(6标一工区)`**:本机列 `_row_id`,线上曾无 `id`;push 默认 pk=`id` 并注入该列 → `has no column named id`;整队曾因 `mark_error` 未改 status 卡住(宇恒已修)。修复路径:删表重建 / 部署 Z10d 后 ensure 补 `id` 再推。
|
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`。
|
||||||
|
|
||||||
**验收**
|
**验收**
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user