feat(platform): close AI expense value loop
Add tenant-safe value, telemetry, connector, commercial, and production-readiness foundations.
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
# Agent 资产多租户隔离与安全规则编辑 概念文档
|
||||
|
||||
更新时间:2026-07-17
|
||||
|
||||
## 功能一句话
|
||||
|
||||
让规则、技能、MCP、任务及其版本、审核、测试和编辑会话都拥有可验证的租户归属,并以“企业资产可写、平台资产只读”的双层模型安全贯通规则生成、真实场景验证、审核、发布和 ONLYOFFICE 编辑。
|
||||
|
||||
## 背景与问题
|
||||
|
||||
原 AgentAsset 数据模型没有结构化 `tenant_id` 与 `scope`,部分读取和内部查询可以在没有用户上下文时返回全局资产。版本、审核、测试、反馈和发布链路主要依赖 `asset_id` 或业务约定关联,无法由数据库阻止跨租户子记录注入。同编码资产也不能由不同企业独立维护。
|
||||
|
||||
规则场景测试可以在未声明目标企业时抽取费用数据,审核主体还可能使用客户端传入的 actor/reviewer 字段,导致测试证据和盲审身份缺乏稳定、可追溯的企业边界。
|
||||
|
||||
规则表的 ONLYOFFICE 内容与回调接口原先缺少持久化的一次性会话。回调下载地址、文档 key、版本、租户和编辑权限之间没有不可变绑定,存在匿名读取、跨资产回写、身份伪造、重放和服务端请求伪造风险。
|
||||
|
||||
## 目标与非目标
|
||||
|
||||
### 目标
|
||||
|
||||
- AgentAsset、Version、Review、TestRun、RuleFeedback 和 ONLYOFFICE Session 均保存结构化租户作用域。
|
||||
- 租户用户只能看到本企业资产和平台资产;同编码时企业资产优先覆盖平台默认资产。
|
||||
- 跨租户详情、版本、审核、发布、测试和反馈统一表现为不存在,避免泄露资源存在性。
|
||||
- 平台资产对所有租户只读,仅平台管理员可新增、修改、发布或编辑。
|
||||
- 所有 HTTP 入口使用认证会话中的 `CurrentUserContext.tenant_id`,不接受客户端覆盖租户。
|
||||
- 写入、版本和审核操作使用 RuleEditor、RuleReviewer 或平台管理员权限,并以稳定身份写入审计证据。
|
||||
- 真实风险场景必须显式声明当前企业 `target_tenant_id`,费用样本的第一层 SQL 条件就是该租户。
|
||||
- ONLYOFFICE 内容和回调使用不同 audience/scope 的签名 token,并绑定数据库一次性会话。
|
||||
- 回调下载拒绝错误 origin、非公网解析、DNS 重绑定、重定向、超限和非安全 OOXML 文件。
|
||||
|
||||
### 非目标
|
||||
|
||||
- 不允许普通企业用户创建或修改平台资产。
|
||||
- 不把所有企业资产放在全局结果集中后仅靠前端过滤。
|
||||
- 不允许平台管理员借普通企业会话修改其他企业的私有资产;跨企业运维需要独立受控流程。
|
||||
- 不提供允许私网、回环或任意下载地址的 ONLYOFFICE 安全降级开关。
|
||||
- 不在本切片中重做 Agent 资产管理前端视觉或商业定价页面。
|
||||
|
||||
## 用户与场景
|
||||
|
||||
- 企业规则编辑者:维护本企业规则资产、上传规则表并创建新版本。
|
||||
- 企业规则审核者:以稳定登录身份进行盲审、驳回或批准规则版本。
|
||||
- 企业风控人员:用本企业真实费用申请生成测试样本和质量证据。
|
||||
- 普通企业用户:读取本企业资产和平台只读资产,但不能写入。
|
||||
- 平台管理员:维护跨企业可见的平台基础规则和模板。
|
||||
- 运行时与调度器:在显式租户范围内加载、测试、监控和召回资产,不进行全局扫描。
|
||||
- ONLYOFFICE 文档服务:使用资源专用 token 读取一次文档,并通过单次 callback session 回写允许编辑的当前版本。
|
||||
|
||||
## 功能能力
|
||||
|
||||
- 双层可见性:`tenant:{tenant_id}` 与 `platform:platform`。
|
||||
- 企业内唯一编码:数据库唯一键为 `(tenant_id, scope, code)`,不同企业可拥有同编码资产。
|
||||
- 确定性覆盖:按编码加载时先找当前企业资产,再回退平台资产。
|
||||
- 服务端可信租户:HTTP 服务由登录会话构造 `AgentAssetAccessScope`;无用户上下文的内部读取只允许平台作用域。
|
||||
- 稳定审计主体:优先记录 `employee:{employee_id}`,没有员工 ID 时记录大小写归一的 `username:{username}`。
|
||||
- 真实样本隔离:场景请求必须传 `target_tenant_id`,且必须等于登录企业;TestRun 记录样本所属企业。
|
||||
- 平台资产可在企业场景中验证,但测试证据仍归目标企业,不能变成平台或其他企业事实。
|
||||
- ONLYOFFICE content token 有效期 15 分钟,callback session 有效期 4 小时。
|
||||
- 回调状态机保证同一个 JTI 最多一次进入写入阶段。
|
||||
|
||||
## 方案设计
|
||||
|
||||
### 前端契约
|
||||
|
||||
- AgentAsset DTO 提供 `tenantId` 与 `scope`,前端可明确标识企业资产和平台资产。
|
||||
- 平台资产在非平台管理员会话中必须隐藏或禁用修改、发布、上传和 ONLYOFFICE 编辑动作。
|
||||
- 场景测试请求必须携带 `targetTenantId`;它是目标企业的显式确认,不是可切换企业的授权参数。
|
||||
- 旧 `X-Actor`/reviewer 头仅为兼容保留,服务端忽略其身份值并使用登录会话主体。
|
||||
- ONLYOFFICE 配置根据权限返回 `view` 或 `edit`;内容和回调 token 只供文档服务使用。
|
||||
|
||||
### 后端职责
|
||||
|
||||
- `agent_asset_scope`:定义 platform/tenant 常量和合法作用域基础规则。
|
||||
- `agent_asset_access`:从可信用户构造访问范围、生成稳定主体并提供可见/可写谓词。
|
||||
- `agent_asset` repository:所有列表、详情、版本、审核、测试和反馈查询都注入结构化租户条件。
|
||||
- `agent_assets`:编排资产 CRUD、版本、审核与序列化,不通过未过滤 ORM 关系返回子记录。
|
||||
- `agent_asset_risk_rule_testing`:校验目标企业、先按租户过滤 ExpenseClaim,再生成测试证据。
|
||||
- 发布、监控、召回、调度、遥测和风险运行时服务:沿资产租户作用域读取和写入,禁止全局 asset id 查询。
|
||||
- `agent_asset_onlyoffice_security`:签发/验证持久化会话并原子消费 callback JTI。
|
||||
- `agent_asset_onlyoffice`:按会话中的租户、资产、key、版本和指纹定位文档,安全下载后创建新版本。
|
||||
- `knowledge_onlyoffice_security`:复用统一安全下载器,执行 origin、DNS/IP、响应和 OOXML 校验。
|
||||
|
||||
### 数据
|
||||
|
||||
`20260717_0026_agent_asset_tenant_security.py` 负责以下结构:
|
||||
|
||||
- `agent_assets`:新增 `tenant_id`、`scope`,建立作用域约束、租户外键和企业内 code 唯一键。
|
||||
- `agent_asset_versions`、`agent_asset_reviews`:新增租户作用域,并以 `(tenant_id, scope, asset_id)` 复合外键绑定父资产。
|
||||
- `agent_asset_test_runs`、`agent_asset_rule_feedback`:保存证据所属企业;平台资产的企业测试/反馈也归企业事实域。
|
||||
- `agent_asset_onlyoffice_sessions`:保存 JTI、租户、资源 scope、asset、document key/version/fingerprint、audience、权限、actor、过期时间和状态。
|
||||
- 旧资产默认回填为平台资产;能从父资产确定的历史版本、审核和证据同步回填。
|
||||
- 无法确认租户的历史数据或不完整旧表结构会 fail-closed,不猜测企业归属。
|
||||
|
||||
### 权限与信任边界
|
||||
|
||||
- 当前企业只来自 `CurrentUserContext.tenant_id`;空值、`platform` 伪企业或请求参数不能构造企业访问范围。
|
||||
- 租户读取谓词是“当前企业或平台”,写入谓词只允许当前企业;平台写入还要求 `is_admin=true`。
|
||||
- 跨租户资源统一返回 404;权限不足的本作用域操作返回受控错误。
|
||||
- RuleEditor 可维护规则和版本,RuleReviewer 执行审核;平台资产的任意写操作额外要求平台管理员。
|
||||
- 版本 created_by、审核 reviewer、规则表变更 actor 都由稳定登录主体生成。
|
||||
- 后台 bootstrap/foundation 按平台作用域精确查找种子资产,不能误改同编码企业资产。
|
||||
- 风险运行时按“企业优先、平台回退”加载发布规则,不扫描其他企业版本。
|
||||
- ORM 关系不是授权边界;对外响应必须经过带 scope 的 repository/service 查询。
|
||||
|
||||
### 资产解析顺序
|
||||
|
||||
```text
|
||||
find_by_code(code, current_tenant):
|
||||
1. tenant_id = current_tenant AND scope = tenant
|
||||
2. tenant_id = platform AND scope = platform
|
||||
3. not found
|
||||
```
|
||||
|
||||
该顺序让企业能够在不修改平台模板的情况下覆盖默认规则,同时保持其他企业和平台资产不受影响。
|
||||
|
||||
### 风险场景测试
|
||||
|
||||
```text
|
||||
认证用户 tenant
|
||||
→ 校验 target_tenant_id == tenant
|
||||
→ 校验目标资产为 tenant 自有或 platform 只读资产
|
||||
→ SQL 第一层条件 ExpenseClaim.tenant_id == target_tenant_id
|
||||
→ 应用时间、费用类型、城市等业务筛选
|
||||
→ 创建 tenant-scoped TestRun
|
||||
```
|
||||
|
||||
没有目标企业、目标企业不一致或资产属于其他企业时均拒绝,不使用 mock 的默认企业或全局样本补齐。
|
||||
|
||||
### ONLYOFFICE 状态机
|
||||
|
||||
```text
|
||||
issue(view) → active ── callback status 2/6 ──拒绝写入
|
||||
issue(edit) → active ── atomic claim ──→ processing
|
||||
├─校验/下载/写入成功→ consumed
|
||||
└─任一步失败────────→ failed
|
||||
|
||||
active -- exp 超时 --> 验证拒绝
|
||||
processing/consumed/failed/revoked -- replay --> 409/拒绝
|
||||
```
|
||||
|
||||
token 同时绑定 issuer、audience、scope、JTI、tenant、resource scope、asset、document key、version、fingerprint、writable、actor、iat/nbf/exp。回调 payload 只能提供状态和下载位置,不能覆盖这些授权事实。
|
||||
|
||||
### 降级与回滚策略
|
||||
|
||||
- 无可信租户:HTTP 请求拒绝;无用户上下文的内部服务只看平台资产,绝不回退全局查询。
|
||||
- 跨租户资产、版本或测试证据:按不存在处理,不尝试平台管理员越权兼容。
|
||||
- 场景样本为空:返回空样本测试事实,不改查其他企业数据。
|
||||
- ONLYOFFICE token、key、版本、指纹、DNS、MIME 或 OOXML 校验失败:拒绝回写并保持原文件不变。
|
||||
- 生产文档服务必须使用配置白名单中的公网 TLS origin,或经满足相同约束的安全代理访问。
|
||||
- migration downgrade 在存在企业资产/证据或 ONLYOFFICE 会话时拒绝有损回滚;必须先通过受控数据迁移清理事实。
|
||||
|
||||
## 测试方案
|
||||
|
||||
- 可见性:两企业同编码资产、企业覆盖平台、平台只读、跨企业详情 404、无上下文仅平台。
|
||||
- 数据完整性:scope/tenant check、企业内 code 唯一、Version/Review 复合父子外键、TestRun 证据企业。
|
||||
- 权限与身份:RuleEditor/RuleReviewer、平台管理员、伪造 actor/reviewer 无效、稳定 employee/username 主体。
|
||||
- 风险场景:必须目标企业、目标不一致拒绝、SQL 租户首过滤、平台资产的企业 TestRun。
|
||||
- 发布链路:发布门禁、监控、召回、运行时、调度和遥测均使用显式租户。
|
||||
- ONLYOFFICE:content/callback scope、tenant/asset/key/version/fingerprint 绑定、平台只读、原子 claim、失败终态和重放拒绝。
|
||||
- SSRF/文件:白名单 origin、全量公网 DNS、固定已校验 IP、拒绝重定向、大小/MIME/ZIP/OOXML 限制。
|
||||
- 迁移:旧平台数据回填、同编码多企业、非法 scope/跨租户外键拒绝、事实存在时 downgrade 拒绝、清理后可回滚。
|
||||
- 所有后端验证只在 `local-x-financial-linux` 容器中执行,单条命令限制 60 秒。
|
||||
|
||||
## 指标与验收
|
||||
|
||||
- 所有 AgentAsset 业务读取都能由 `tenant_id + scope` 确定结果范围。
|
||||
- 跨企业资产、版本、审核、测试、反馈和回调用例 100% 不可见或拒绝。
|
||||
- 平台资产对非平台管理员的写入和回调 100% 无副作用。
|
||||
- 真实费用样本查询 100% 具有服务端校验的企业首过滤条件。
|
||||
- 审计主体 100% 来自稳定登录身份,客户端 actor/reviewer 不能改变事实。
|
||||
- 同一 ONLYOFFICE callback JTI 最多一次进入 `processing`。
|
||||
- 相关 Python 文件通过 Ruff、py_compile 和 ORM mapper 配置;定向发布、安全与风险规则回归通过。
|
||||
|
||||
## 风险与开放问题
|
||||
|
||||
- 旧测试或内部调用若直接构造没有 `tenant_id` 的 `CurrentUserContext`,会按新信任边界 fail-closed;应由对应调用方补齐真实租户,而不是放宽服务契约。
|
||||
- 两个旧费用风险测试使用尚未持久化、没有 claim tenant 的对象;共享租户作用域已要求先保存并确认归属,需由费用申请切片统一调整测试夹具。
|
||||
- 两个旧风险发布测试手工注入 aggregate 后直接 promote,与当前必须有真实质量 TestRun 的发布门禁不一致;需由发布门禁切片统一口径。
|
||||
- ONLYOFFICE 实际回写依赖部署环境提供公网可解析、可信 TLS 的文档服务或安全代理;开发网络解析到保留/私网地址时会按设计拒绝。
|
||||
- 企业间受控复制、平台资产签名发布和跨企业运维审计属于后续独立能力,不应通过放宽本轮隔离实现。
|
||||
|
||||
## 本轮实现记录
|
||||
|
||||
- 2026-07-17:完成 AgentAsset、Version、Review、TestRun、Feedback 的结构化租户作用域、企业覆盖平台读取和跨企业 fail-closed。
|
||||
- 2026-07-17:完成可信目标企业风险场景、真实费用样本 SQL 首过滤、企业 TestRun 证据和稳定盲审身份。
|
||||
- 2026-07-17:完成 DB-backed ONLYOFFICE 一次性会话、平台只读、文档基线绑定与安全下载。
|
||||
- 2026-07-17:完成 `20260717_0026` 迁移及一次性 PostgreSQL `base → head`、`head → 0025 → head` 验证。
|
||||
- 2026-07-17:完成发布、监控、召回、运行时、调度、遥测、foundation 和风险规则生成链路的租户接线与容器回归。
|
||||
Reference in New Issue
Block a user