BISHENG 多租户资源所有者交接(F018)设计与实现指南
2026/9/15 11:54:25 网站建设 项目流程

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 双写事务如何保证一致性、以及 19601~19606 错误码各自触发的条件与规避方式。


1. 特性背景与用户故事

在多租户体系中,员工离职、调岗是高频场景。PRD Review P0-C 决策"用户归属跟主部门切,数据留原处不动"——即用户的主部门(叶子 Tenant)变更时,其名下资源并不跟随迁移。这一决策虽然避免了数据搬运的复杂性,却带来一个必须配套解决的新问题:

谁来接盘离职/调岗员工在某个 Tenant 下遗留的资源?

F018 正是为此设计的所有者交接能力。其用户故事可以概括为:

作为离职员工 / 调岗员工 / 管理员, 我希望将某用户在本 Tenant 下的资源所有权批量转交给其他成员, 以便调岗/离职时不留孤儿资源,保障集团核心知识库的连续性

在 BISHENG 中,这意味着一次接口调用同时完成两类所有权变更:

  1. MySQL 数据面:批量更新 7 类资源表的user_id字段;
  2. 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 错误码19601
AC-04调用者指定resource_ids=null自动转交from_usertenant_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.1)
AC-08bfrom 在 Child A、to 在 Root(同 Root 子树)允许;典型"交回总部"场景;FGA owner 元组直接转移
AC-08cfrom 在 Child A、to 在 Child B(同 Root 但不同 Child)拒绝;to_user叶子=Child B ∉ {Child A, Root};返回19603
AC-08dfrom 在 Root、to 在 Child(资源下沉场景)拒绝;资源tenant_id=Root 时可见集合 = {Root};to_user叶子=Child ∉ 集合;返回19603;响应体提示走 F011 迁移 API
AC-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_userto_user都在tenant_id✅ 典型"同事接手"场景
Child → Rootfrom_user在 Child,to_user在 Root✅ 典型"交回总部"场景
Root → Child(资源下沉)❌ 2026-04-21 移除;走 F011 迁移 API,不走本接口

从源码实现看,可见集合校验与上述路径一一对应(resource_ownership_service.py):

@classmethod async def _check_receiver_visible(cls, to_user_id: int, tenant_id: int) -> None: """AC-08 / INV-T10: receiver's 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)、拒绝"横向跨 Child"(AC-08c)。接收人叶子 Tenant 的解析依赖UserTenantDao.aget_active_user_tenant(F011 提供),无活动记录时回退到 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}不含 Child,to_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. 架构决策(AD)

ID决策项选项结论
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-02MAX_BATCH = 500常量。注释说明了权衡依据:500 条既保持在 MySQLmax_allowed_packet之内,又处于 OpenFGA 单请求 100 条上限的可分块范围内(PermissionService内部会自动分块)。
  • AD-03→ 事务性双写:MySQL 批量 UPDATE 与 FGA 元组翻转在同一流程内完成,任一步失败抛出19605表示"已全部回滚";FGA 写入使用crash_safe=True,在调用 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 gate(AC-03 → 19601) cls._check_operator(operator, from_user_id) # 3. Receiver visible-set check(INV-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_id(19606)与未知资源类型(19604)在触碰数据库之前就短路,避免无谓的 SQL 开销。
  • 解析后才校验批量上限:AC-07 的 500 条上限作用于"实际解析出的资源数",而非请求里的resource_ids长度。
  • 空结果快速返回resources为空时返回transferred_count=0,不产生任何 FGA 或审计副作用(测试test_receiver_child_to_root_allowed专门验证了这一点)。

5.2 资源解析与批量更新

_resolve_resourcesbypass_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()后重新抛出。值得一提的细节是:folderknowledge_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.DIR
knowledge_fileknowledgefileintfile_type = 1FileType.FILE
workflowflowstr(UUID)flow_type = 10FlowType.WORKFLOW
assistantassistantstr(UUID)独立表
toolt_gpts_toolsintis_delete = 0排除墓碑记录
channelchannelstr(CHAR(36))独立表

该注册表同时约束了两层防线:

  1. 服务层get_meta()对未知类型抛19604
  2. DTO 层:请求模型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(action='delete', user=f'user:{from_user_id}', relation='owner', object=f'{r.resource_type}:{r.id}')) ops.append(TupleOperation(action='write', user=f'user:{to_user_id}', relation='owner', object=f'{r.resource_type}:{r.id}')) await PermissionService.batch_write_tuples(ops, crash_safe=True)

crash_safe=True是 F004(REBAC 核心)提供的崩溃安全双写能力:先预写failed_tuples,再调 FGA;FGA 成功后清除预写记录。若进程在 MySQL 提交之后、FGA 生效之前崩溃,补偿 worker 会依据failed_tuples重放,满足 AC-06 的"最终一致"诉求。

5.5 审计日志

审计动作常量定义于 tenant/domain/constants.py:RESOURCE_TRANSFER_OWNER = 'resource.transfer_owner'。审计写入是best-effort_safe_audit):主事务已提交,审计失败只记日志、不阻断成功返回。operator_tenant_id的解析规则为:优先取admin_scope_tenant_id(F019 全局超管切换管理视图);否则超管默认 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> 0
to_user_idint> 0,且不得等于from_user_id
resource_typesstring[]必须为 7 类注册类型之一,min_length=1
resource_idsstring[]可选;null表示转交全部可转移资源;混用 int/UUID 字符串均可(服务层按表id_type自动过滤)
reasonstring可选,max_length=1000

成功响应:

{"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:

错误码含义HTTP
19601无权限转交(非 owner 且非 admin)403
19602超过批量上限(500 条)400
19603接收人叶子 Tenant 不在资源 tenant_id 的可见集合内400
19604资源类型不支持交接400
19605MySQL/OpenFGA 事务失败已回滚500
19606from_user_id 等于 to_user_id400

错误码类定义集中在 resource_owner_transfer.py,错误码模块MMM=196 (resource_owner_transfer),每个类的CodeMsg与上表一一对应;19605之所以映射 500,是让调用方能够区分"事务性失败(可重试前先排查 FGA 健康)"与"其他未处理异常"。


7. 依赖关系

F018 自身实现 Root→Child 下沉逻辑(AC-08d 命中场景需引导调用方走 F011 迁移 API),其依赖如下:

  • F011-tenant-tree-model:Tenant 模型 +POST /api/v1/tenants/{child_id}/resources/migrate-from-root资源下沉端点(INV-T10);同时提供接收人叶子 Tenant 解析所需的UserTenantDao.aget_active_user_tenant
  • F013-tenant-fga-tree:FGAbatch_write能力;
  • v2.5.0/F004-rebac-coreFailedTuple补偿机制(crash_safe=True双写)。

服务实现中刻意保持最小依赖:超管判定优先走operator.is_global_super(),不存在时回退is_admin()(F011 mount 服务采用同一 guard,F013 会进一步收紧为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 路径(解析、批量更新、待交接列表)走真实 SQLite;FGA 与审计 DAO 用AsyncMockUserTenantDao.aget_active_user_tenant按用例打桩以控制接收人叶子(AC-08 各分支)。关键用例包括:

  • test_self_transfer_rejected_19606:from == to → 19606;
  • test_unsupported_type_rejected_19604dashboard(已移出 MVP)→ 19604;
  • test_non_owner_non_admin_rejected_19601:闯入者转交他人资源 → 19601;
  • test_receiver_cross_child_rejected_19603/test_receiver_root_to_child_rejected_19603:AC-08c/08d;
  • test_receiver_child_to_root_allowed:AC-08b,无资源时 0 转移、FGA 与审计均不触发;
  • test_batch_over_500_rejected_19602:种入 501 条 workflow → 19602;
  • test_resource_ids_null_selects_all_from_user/test_resource_ids_list_filters_to_named:AC-04/05;
  • test_happy_path_updates_mysql_fga_audit:验证 MySQLuser_id翻转、FGA 恰好 2 删 2 写、审计action='resource.transfer_owner'与 reason 透传;
  • test_fga_failure_rolls_back_and_raises_19605:FGA 抛错 → 19605 且审计不写入;
  • test_pending_includes_users_whose_leaf_moved:AC-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、随机用户 403+19601、resource_ids透传、>500 → 400+19602、跨 Child 与 Root→Child → 400+19603、dashboard→ FastAPI 原生 422、自转 → 400+19606、事务失败 → 500+19605、待交接列表仅 admin/超管可访问。

8.3 UI 项

AC-09(个人中心"我的资源→转交给...")与 AC-10(超管"待交接资源"列表)标注为 Playwright E2E,待前端测试框架搭建后补充;在此之前由开发者本地启前端逐项手动自测并截图附入 PR。


9. 小结

F018 通过"服务层单一事务 + FGA 崩溃安全双写 + 可见集合收口"三件套,为 BISHENG 多租户体系提供了可靠的资源所有者交接能力:

  • 权限收口:owner / tenant admin / 全局超管三道闸门全部收敛在ResourceOwnershipService,HTTP 层只做透传与错误码映射;
  • 路径收口:仅允许"同 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询