BISHENG 多租户资源所有者交接(F018)设计与实现指南
发布时间:2026/9/15 11:54:42 作者:尧图编辑部 阅读量:1,286
设计与实现指南)
BISHENG 多租户资源所有者交接F018设计与实现指南【免费下载链接】bishengBISHENG is an open LLM devops platform for next generation Enterprise AI applications. Powerful and comprehensive features include: GenAI workflow, RAG, Agent, Unified model management, Evaluation, SFT, Dataset Management, Enterprise-level System Management, Observability and more.项目地址: https://gitcode.com/GitHub_Trending/bi/bisheng本文围绕 BISHENG开源 LLM DevOps 平台v2.5.1 版本中的F018-resource-owner-transfer所有者交接 API特性规格展开完整讲解其用户故事、验收标准、交接路径约束、架构决策、核心服务实现、HTTP API 设计、错误码体系与自测清单并结合当前仓库的真实源码与测试用例进行纵深佐证。读完本文你将掌握如何通过transfer-owner接口在 Tenant 内批量转交资源所有权、可见集合INV-T10规则如何工作、MySQL 与 OpenFGA 双写事务如何保证一致性、以及 1960119606 错误码各自触发的条件与规避方式。1. 特性背景与用户故事在多租户体系中员工离职、调岗是高频场景。PRD Review P0-C 决策用户归属跟主部门切数据留原处不动——即用户的主部门叶子 Tenant变更时其名下资源并不跟随迁移。这一决策虽然避免了数据搬运的复杂性却带来一个必须配套解决的新问题谁来接盘离职/调岗员工在某个 Tenant 下遗留的资源F018 正是为此设计的所有者交接能力。其用户故事可以概括为作为离职员工 / 调岗员工 / 管理员 我希望将某用户在本 Tenant 下的资源所有权批量转交给其他成员 以便调岗/离职时不留孤儿资源保障集团核心知识库的连续性。在 BISHENG 中这意味着一次接口调用同时完成两类所有权变更MySQL 数据面批量更新 7 类资源表的user_id字段OpenFGA 权限面批量删除原 owner 的owner元组、写入新 owner 的owner元组。两者必须一致成功否则会出现数据归新人、权限还挂在老人名下的撕裂状态——这正是本特性核心架构决策AD-03要解决的问题。2. 验收标准AC总览F018 的规格文档定义了 11 条验收标准覆盖权限、批量、可见性、事务与 UI 等维度ID角色操作预期结果AC-01资源原 ownerPOST /tenants/{tid}/resources/transfer-owner自己转交成功返回transferred_countaudit_log记录AC-02Child Admin代替下属 owner 转交成功tenant admin 权限放行AC-03非 owner 非 admin尝试转交他人资源HTTP 403 错误码19601AC-04调用者指定resource_idsnull自动转交from_user在tenant_id下所有可转移资源AC-05调用者指定resource_ids列表仅转交列表中的资源AC-06开发MySQL 写成功但 OpenFGA 失败事务回滚写入failed_tuples队列重试AC-07调用者单次请求 500 条HTTP 400 错误码19602提示分批AC-08调用者to_user叶子 Tenant 不在tenant_id可见集合内拒绝 HTTP 400 错误码19603可见集合 {tenant_id, tenant_id 的 Root}INV-T10 / PRD §5.6.3.1AC-08b—from 在 Child A、to 在 Root同 Root 子树允许典型交回总部场景FGA owner 元组直接转移AC-08c—from 在 Child A、to 在 Child B同 Root 但不同 Child拒绝to_user叶子Child B ∉ {Child A, Root}返回19603AC-08d—from 在 Root、to 在 Child资源下沉场景拒绝资源tenant_idRoot 时可见集合 {Root}to_user叶子Child ∉ 集合返回19603响应体提示走 F011 迁移 APIAC-09用户个人中心访问我的资源→转交给... UI可批量选择资源 接收人AC-10全局超管访问待交接资源列表展示离职/超期未交接的资源清单设计要点AC-01 与 AC-02 说明交接的主体是资源原 owner或具备 admin 权限的代发者AC-03 明确非 owner 非 admin 的第三方一律 403。权限判定完全收敛在服务层HTTP 层只做映射详见第 5 节源码佐证。3. 支持的交接路径与边界情况3.1 支持的交接路径INV-T10 / PRD §5.6.3.1路径场景结论同 Tenant 内from_user与to_user都在tenant_id✅ 典型同事接手场景Child → Rootfrom_user在 Childto_user在 Root✅ 典型交回总部场景Root → Child资源下沉—❌ 2026-04-21 移除走 F011 迁移 API不走本接口从源码实现看可见集合校验与上述路径一一对应resource_ownership_service.pyclassmethod async def _check_receiver_visible(cls, to_user_id: int, tenant_id: int) - None: AC-08 / INV-T10: receivers leaf tenant must be in {tenant_id, ROOT_TENANT_ID}. leaf await cls._resolve_leaf_tenant(to_user_id) allowed {tenant_id, ROOT_TENANT_ID} if leaf not in allowed: raise ResourceTransferReceiverOutOfTenantError()当tenant_id为 Root 时allowed {Root}Child 叶子用户必然被拒AC-08d当tenant_id为 Child 时allowed {Child, Root}恰好放行交回总部AC-08b、拒绝横向跨 ChildAC-08c。接收人叶子 Tenant 的解析依赖UserTenantDao.aget_active_user_tenantF011 提供无活动记录时回退到 Root例如新注册尚未 SSO 同步的用户。3.2 边界情况清单transfer_types不含的资源忽略例如 session / message 不支持 owner。to_user已是 owner幂等 skip。部分资源无 owner 元组跳过并记录可能是 v2.4 遗留未迁移资源记入告警。不支持的场景跨 Root 子树交接即不支持把资源从一个集团转给另一个集团本实例只有一个 Root物理上不存在此场景同 Root 内 Child A → Child B 直接交接应先 Child A → Root 交回总部再通过 F011/tenants/{B_id}/resources/migrate-from-root迁移tenant_id两步走避免出现飞地资源Root → Child资源下沉本 API 的tenant_id参数 资源所在 Tenant Root其可见集合 {Root}不含 Childto_user在 Child 会被 AC-08 拒绝。资源下沉请走 F011POST /api/v1/tenants/{child_id}/resources/migrate-from-root接口改资源tenant_id再于 Child 内交接 owner交接后撤回需二次交接回去自动调岗时转交用户须主动发起或 Admin 代发。注意AC-08d 是 2026-04-21 对原规格 §3 的修正——早期版本声称支持 Root→Child 下沉这与 AC-08 的可见集合规则自相矛盾因此统一收窄为拒绝 引导走 F011。4. 架构决策ADID决策项选项结论AD-01交接操作权限A: 仅 owner 本人 / B: 本人 Admin 代发B支持离职场景AD-02批量上限A: 无限制 / B: 500 条 / C: 100 条B权衡 FGA Write 性能AD-03失败策略A: 整批回滚 / B: 局部成功 补偿A避免部分交接导致的不一致三项决策在源码中均有明确落点AD-01→_check_operator三道闸门owner 本人放行 →is_global_super放行 →is_admin放行否则抛19601见 resource_ownership_service.py。AD-02→MAX_BATCH 500常量。注释说明了权衡依据500 条既保持在 MySQLmax_allowed_packet之内又处于 OpenFGA 单请求 100 条上限的可分块范围内PermissionService内部会自动分块。AD-03→ 事务性双写MySQL 批量 UPDATE 与 FGA 元组翻转在同一流程内完成任一步失败抛出19605表示已全部回滚FGA 写入使用crash_safeTrue在调用 FGA 前先预写failed_tuples进程若在 MySQL 提交后、FGA 写入前崩溃由 F004 的补偿 worker 从failed_tuples重放。5. 核心服务实现源码级规格 §5 给出了ResourceOwnershipService的设计骨架仓库中的真实实现位于 resource_ownership_service.py文件头注释明确标注Implements spec AC-01~AC-10 / AC-08b/c/d。5.1 服务入口与校验流水线transfer_owner的完整调用顺序如下classmethod async def transfer_owner( cls, tenant_id: int, from_user_id: int, to_user_id: int, resource_types: List[str], resource_ids: Optional[List[Union[int, str]]] None, reason: str , operator: Any None, ) - Dict[str, Any]: # 1. Fast validations先于任何 DB I/O if from_user_id to_user_id: raise ResourceTransferSelfError() # 19606 for rt in resource_types: get_meta(rt) # 19604 未知类型 # 2. Operator permission gateAC-03 → 19601 cls._check_operator(operator, from_user_id) # 3. Receiver visible-set checkINV-T10 / AC-08 → 19603 await cls._check_receiver_visible(to_user_id, tenant_id) # 4. Resolve resources owned by from_user within tenant resources await cls._resolve_resources( tenant_id, from_user_id, resource_types, resource_ids, ) if len(resources) MAX_BATCH: raise ResourceTransferBatchLimitError() # 19602 if not resources: return {transferred_count: 0, transfer_log_id: None} # 5. Transactional flip: MySQL → OpenFGA (crash_safe) → audit transfer_log_id cls._make_transfer_log_id() try: await cls._bulk_update_user_ids(resources, to_user_id) await cls._flip_fga_owner_tuples(resources, from_user_id, to_user_id) except Exception as exc: raise ResourceTransferTxFailedError() from exc # 19605 await cls._safe_audit(...) return {transferred_count: len(resources), transfer_log_id: transfer_log_id}设计上的几个关键取舍先校验后 I/Ofrom_user_id to_user_id19606与未知资源类型19604在触碰数据库之前就短路避免无谓的 SQL 开销。解析后才校验批量上限AC-07 的 500 条上限作用于实际解析出的资源数而非请求里的resource_ids长度。空结果快速返回resources为空时返回transferred_count0不产生任何 FGA 或审计副作用测试test_receiver_child_to_root_allowed专门验证了这一点。5.2 资源解析与批量更新_resolve_resources在bypass_tenant_filter()上下文中对每种资源类型执行SELECT id, user_id, tenant_id FROM {meta.table} WHERE user_id :uid AND tenant_id :tid [AND {meta.type_filter_sql}] [AND id IN :ids] -- 仅当显式指定 resource_ids 时批量更新则按类型分组对每张表执行UPDATE ... SET user_id :uid WHERE id IN :ids最后统一commit()异常时rollback()后重新抛出。值得一提的细节是folder与knowledge_file共享物理表knowledgefile通过各自的type_filter_sql区分file_type 0/file_type 1因此分组维度是resource_type而非物理表名保证 SELECT 与 UPDATE 的 SQL 对称、审计计数与类型口径一致。5.3 资源类型注册表7 类 MVP规格指出 MVP 收窄为 7 类资源Dashboard 因暂无 ORM 延后。仓库用 resource_type_registry.py 作为唯一事实源未来新增类型只需在此注册resource_type物理表id_typetype_filter_sql说明knowledge_spaceknowledgeinttype 3仅 KnowledgeTypeEnum.SPACE 可转移排除 chat/QA/private 变体folderknowledgefileintfile_type 0FileType.DIRknowledge_fileknowledgefileintfile_type 1FileType.FILEworkflowflowstr(UUID)flow_type 10FlowType.WORKFLOWassistantassistantstr(UUID)无独立表toolt_gpts_toolsintis_delete 0排除墓碑记录channelchannelstr(CHAR(36))无独立表该注册表同时约束了两层防线服务层get_meta()对未知类型抛19604DTO 层请求模型TransferOwnerRequest.resource_types使用Literal[knowledge_space,folder,knowledge_file,workflow,assistant,tool,channel]做 FastAPI 原生校验未知值在进入 handler 之前就返回 HTTP 422测试test_unsupported_type_rejected_at_dto_level验证了dashboard被拒。另外_coerce_ids会静默丢弃与目标表id_type不匹配的 id例如把数字 id 传给 UUID 表的场景避免ValueError的同时保持查不到即不转移的语义。5.4 FGA owner 元组翻转_flip_fga_owner_tuples为每个资源构造一对操作ops.append(TupleOperation(actiondelete, userfuser:{from_user_id}, relationowner, objectf{r.resource_type}:{r.id})) ops.append(TupleOperation(actionwrite, userfuser:{to_user_id}, relationowner, objectf{r.resource_type}:{r.id})) await PermissionService.batch_write_tuples(ops, crash_safeTrue)crash_safeTrue是 F004REBAC 核心提供的崩溃安全双写能力先预写failed_tuples再调 FGAFGA 成功后清除预写记录。若进程在 MySQL 提交之后、FGA 生效之前崩溃补偿 worker 会依据failed_tuples重放满足 AC-06 的最终一致诉求。5.5 审计日志审计动作常量定义于 tenant/domain/constants.pyRESOURCE_TRANSFER_OWNER resource.transfer_owner。审计写入是best-effort_safe_audit主事务已提交审计失败只记日志、不阻断成功返回。operator_tenant_id的解析规则为优先取admin_scope_tenant_idF019 全局超管切换管理视图否则超管默认 Root、普通 admin/用户取自身叶子未知形状回退资源tenant_id。6. HTTP API 设计规格 §6 定义了两个端点真实路由实现在 resource_owner_transfer.py。6.1 POST 转交接口POST /api/v1/tenants/{tenant_id}/resources/transfer-owner Auth: 资源 owner 本人 OR tenant admin OR 全局超管请求体对应TransferOwnerRequest见 tenant_schema.py{ from_user_id: 123, to_user_id: 456, resource_types: [knowledge_space, workflow], resource_ids: null, reason: 张三调岗到子公司 X }字段约束字段类型约束from_user_idint 0to_user_idint 0且不得等于from_user_idresource_typesstring[]必须为 7 类注册类型之一min_length1resource_idsstring[]可选null表示转交全部可转移资源混用 int/UUID 字符串均可服务层按表id_type自动过滤reasonstring可选max_length1000成功响应{transferred_count: 18, transfer_log_id: txn_20260420_abc123}transfer_log_id格式为txn_YYYYMMDD_8hex源码_make_transfer_log_id与 PRD §5.6.3.1 示例txn_20260420_abc123一致可作为审计日志的target_id。6.2 GET 待交接列表接口GET /api/v1/tenants/{tenant_id}/resources/pending-transfer Auth: 全局超管 / Child admin返回示例{list: [{user_id: 123, user_name: ..., resource_count: 8, relocated_at: ...}]}实现上list_pending_transfer在bypass_tenant_filter()下对 7 张注册表按tenant_id聚合COUNT(*)再逐一比对每个用户的当前叶子 Tenant叶子 ≠tenant_id即判定为已调离却仍持有资源进入待交接列表。规格说明 MVP 不设资源年龄阈值超期过滤由调用方自行实现。该端点会泄露离职/调岗用户的 id因此仅 admin/超管可访问——端点代码明确拒绝普通用户并返回 19601。6.3 错误码 → HTTP 状态映射端点为结构性请求错误映射 400、权限闸门映射 403、事务失败映射 500错误码含义HTTP19601无权限转交非 owner 且非 admin40319602超过批量上限500 条40019603接收人叶子 Tenant 不在资源 tenant_id 的可见集合内40019604资源类型不支持交接40019605MySQL/OpenFGA 事务失败已回滚50019606from_user_id 等于 to_user_id400错误码类定义集中在 resource_owner_transfer.py错误码模块MMM196 (resource_owner_transfer)每个类的Code与Msg与上表一一对应19605之所以映射 500是让调用方能够区分事务性失败可重试前先排查 FGA 健康与其他未处理异常。7. 依赖关系F018 自身不实现 Root→Child 下沉逻辑AC-08d 命中场景需引导调用方走 F011 迁移 API其依赖如下F011-tenant-tree-modelTenant 模型 POST /api/v1/tenants/{child_id}/resources/migrate-from-root资源下沉端点INV-T10同时提供接收人叶子 Tenant 解析所需的UserTenantDao.aget_active_user_tenantF013-tenant-fga-treeFGAbatch_write能力v2.5.0/F004-rebac-coreFailedTuple补偿机制crash_safeTrue双写。服务实现中刻意保持最小依赖超管判定优先走operator.is_global_super()不存在时回退is_admin()F011 mount 服务采用同一 guardF013 会进一步收紧为is_tenant_admin保证 F018 落地不被 F012/F013 阻塞。8. 自测清单与测试验证规格 §8 要求开发者实现后自行运行测试。仓库中已存在两套真实测试可作为回归依据8.1 服务层测试test_resource_ownership_service.py 覆盖 AC-01/03/04/05/06/07/08/08b/08c/08d 与 AD-02/AD-03策略为SQL 路径解析、批量更新、待交接列表走真实 SQLiteFGA 与审计 DAO 用AsyncMockUserTenantDao.aget_active_user_tenant按用例打桩以控制接收人叶子AC-08 各分支。关键用例包括test_self_transfer_rejected_19606from to → 19606test_unsupported_type_rejected_19604dashboard已移出 MVP→ 19604test_non_owner_non_admin_rejected_19601闯入者转交他人资源 → 19601test_receiver_cross_child_rejected_19603/test_receiver_root_to_child_rejected_19603AC-08c/08dtest_receiver_child_to_root_allowedAC-08b无资源时 0 转移、FGA 与审计均不触发test_batch_over_500_rejected_19602种入 501 条 workflow → 19602test_resource_ids_null_selects_all_from_user/test_resource_ids_list_filters_to_namedAC-04/05test_happy_path_updates_mysql_fga_audit验证 MySQLuser_id翻转、FGA 恰好 2 删 2 写、审计actionresource.transfer_owner与 reason 透传test_fga_failure_rolls_back_and_raises_19605FGA 抛错 → 19605 且审计不写入test_pending_includes_users_whose_leaf_movedAC-10叶子已移走的用户被列出test_max_batch_is_500/test_resource_row_is_frozen常量回归守卫与不可变约束。8.2 HTTP 层测试test_resource_owner_transfer_api.py 只挂载 F018 路由的最小 FastAPI 应用Mock 服务层验证路由、鉴权注入、DTO 校验与错误码→HTTP 映射owner 本人 200、Child Admin 200、随机用户 40319601、resource_ids透传、500 → 40019602、跨 Child 与 Root→Child → 40019603、dashboard→ FastAPI 原生 422、自转 → 40019606、事务失败 → 50019605、待交接列表仅 admin/超管可访问。8.3 UI 项AC-09个人中心我的资源→转交给...与 AC-10超管待交接资源列表标注为 Playwright E2E待前端测试框架搭建后补充在此之前由开发者本地启前端逐项手动自测并截图附入 PR。9. 小结F018 通过服务层单一事务 FGA 崩溃安全双写 可见集合收口三件套为 BISHENG 多租户体系提供了可靠的资源所有者交接能力权限收口owner / tenant admin / 全局超管三道闸门全部收敛在ResourceOwnershipServiceHTTP 层只做透传与错误码映射路径收口仅允许同 Tenant 内与Child → Root其余场景一律 19603 拒绝并引导走 F011 迁移 API杜绝跨集团与飞地资源一致性收口MySQLuser_id翻转与 OpenFGA owner 元组翻转成对出现任一步失败整体回滚并上报 19605配合failed_tuples补偿队列兜底进程崩溃窗口。如需深入源码建议按以下路径阅读服务实现 resource_ownership_service.py → 类型注册表 resource_type_registry.py → 端点 resource_owner_transfer.py → 错误码 resource_owner_transfer.py → DTO tenant_schema.py再以两份测试文件作为行为契约回归。【免费下载链接】bishengBISHENG is an open LLM devops platform for next generation Enterprise AI applications. Powerful and comprehensive features include: GenAI workflow, RAG, Agent, Unified model management, Evaluation, SFT, Dataset Management, Enterprise-level System Management, Observability and more.项目地址: https://gitcode.com/GitHub_Trending/bi/bisheng创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考