From 140efd1a2a93299e7e3aed5498f42e5d84d0e6f2 Mon Sep 17 00:00:00 2001 From: caoxiaozhu Date: Mon, 20 Jul 2026 10:30:22 +0800 Subject: [PATCH] fix(agent-assets): allow admin access to platform rules --- ...itor-onlyoffice-runtime-platform-access.md | 14 +++++++ server/src/app/services/agent_asset_access.py | 12 +++++- server/tests/test_agent_asset_service.py | 30 ++++++++++++++ .../tests/test_agent_asset_tenant_security.py | 39 ++++++++++++++++++- 4 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 document/development/2026-07-18/dev-logs/bugs/rule-editor-onlyoffice-runtime-platform-access.md diff --git a/document/development/2026-07-18/dev-logs/bugs/rule-editor-onlyoffice-runtime-platform-access.md b/document/development/2026-07-18/dev-logs/bugs/rule-editor-onlyoffice-runtime-platform-access.md new file mode 100644 index 0000000..c49bb9b --- /dev/null +++ b/document/development/2026-07-18/dev-logs/bugs/rule-editor-onlyoffice-runtime-platform-access.md @@ -0,0 +1,14 @@ +# 规则在线表格缺少运行依赖且平台资产下载被拒绝 + +日期:2026-07-18 +文档路径:document/development/2026-07-18/dev-logs/bugs/rule-editor-onlyoffice-runtime-platform-access.md + +## 修复记录 +- 15:00:记录 bug 修复:规则详情无法加载 ONLYOFFICE,补齐容器后平台规则文件仍因机器会话缺少有效企业而下载失败。 + - Git 提交检查:`git fetch --all --prune` 成功;upstream `origin/main`;upstream 新提交:未发现;本地 ahead 19 条,最新为 `07241b46 fix(docker): manage local postgres in default compose`、`787bc3a4 feat(platform): close AI expense value loop`、`242d68c3 feat(approval): add task workflow and waiver decisions`、`28b834ed fix(approval): replay immutable action responses`、`4940ebc4 feat(approval): add safe risk disposition workflow`、`ee88a36b feat(ai): add tenant-safe hierarchical expense learning`、`6bdf65bc feat(expenses): add authoritative pre-review workflow`、`ae3f02c3 feat(expense): add persistent zero-entry receipt association`,另有 11 条。 + - 原因:默认 Compose 没有管理 ONLYOFFICE DocumentServer,应用配置仍可能被根 `.env` 覆盖;服务启动后,ONLYOFFICE 内容令牌映射出的合法平台管理员机器会话又被资产访问域统一拒绝。 + - 修改:默认 Compose 新增带健康检查和 JWT 的 `onlyoffice` 服务,主应用等待 PostgreSQL 与 ONLYOFFICE 均健康后启动,并分别配置浏览器公共地址和 Compose 网络内回调地址;本地运行配置不再被根 `.env` 悄悄覆盖。 + - 修改:平台资产访问域仅允许真实 `is_admin=true` 的平台会话进入,并把可见范围严格限制为 `scope=platform` 且 `tenant_id=platform`;普通平台会话继续拒绝,企业管理员也不能借此跨企业访问。 + - 操作:通过 `admin/admin` 在系统设置中保存本地 ONLYOFFICE 地址与匹配 JWT 密钥;Compose 重建后仅保留一个应用容器 `local-x-financial-linux`,依赖容器为 PostgreSQL 和 ONLYOFFICE,三者均为 healthy。 + - 验证:规则配置、内容下载和回调链路均返回 200;真实页面成功加载公司通信费报销规则工作簿,显示完整工具栏、工作表“通信费报销标准”和 A1 内容“序号”;平台资产相关回归 36 条通过,最终后端联合回归 `65 passed`,`docker compose config --quiet` 与 `git diff --check` 通过。 + - 影响:在默认 Compose 环境中无需手工启动外部文档服务即可在线查看和编辑规则 Excel,同时平台规则访问仍保持最小权限边界。 diff --git a/server/src/app/services/agent_asset_access.py b/server/src/app/services/agent_asset_access.py index a580ade..9091f07 100644 --- a/server/src/app/services/agent_asset_access.py +++ b/server/src/app/services/agent_asset_access.py @@ -33,11 +33,19 @@ class AgentAssetAccessScope: @classmethod def from_user(cls, current_user: CurrentUserContext) -> AgentAssetAccessScope: tenant_id = str(current_user.tenant_id or "").strip() - if not tenant_id or tenant_id == AGENT_ASSET_PLATFORM_TENANT_ID: + is_platform_admin = bool(current_user.is_admin) + if not tenant_id or ( + tenant_id == AGENT_ASSET_PLATFORM_TENANT_ID and not is_platform_admin + ): raise PermissionError("当前登录会话缺少有效租户。") - return cls(tenant_id=tenant_id, is_platform_admin=bool(current_user.is_admin)) + return cls(tenant_id=tenant_id, is_platform_admin=is_platform_admin) def visibility_clause(self, model: Any) -> Any: + if self.tenant_id == AGENT_ASSET_PLATFORM_TENANT_ID: + return and_( + model.scope == AGENT_ASSET_PLATFORM_SCOPE, + model.tenant_id == AGENT_ASSET_PLATFORM_TENANT_ID, + ) return or_( and_( model.scope == AGENT_ASSET_PLATFORM_SCOPE, diff --git a/server/tests/test_agent_asset_service.py b/server/tests/test_agent_asset_service.py index 03ae0f2..d723a10 100644 --- a/server/tests/test_agent_asset_service.py +++ b/server/tests/test_agent_asset_service.py @@ -693,6 +693,9 @@ def test_platform_spreadsheet_onlyoffice_requires_platform_admin_to_edit(monkeyp admin_callback_token = parse_qs( urlsplit(admin_config.config["editorConfig"]["callbackUrl"]).query )["access_token"][0] + admin_content_token = parse_qs( + urlsplit(admin_config.config["document"]["url"]).query + )["access_token"][0] finance_session = service.validate_rule_spreadsheet_access_token( rule.id, finance_callback_token, @@ -708,6 +711,33 @@ def test_platform_spreadsheet_onlyoffice_requires_platform_admin_to_edit(monkeyp assert admin_session.tenant_id == "platform" assert admin_session.resource_scope == "platform" assert admin_session.actor == "username:platform_admin" + + content_session = service.validate_rule_spreadsheet_access_token( + rule.id, + admin_content_token, + ) + onlyoffice_service = AgentAssetService( + db, + current_user=CurrentUserContext( + username=content_session.actor, + name="ONLYOFFICE", + role_codes=["manager"], + is_admin=True, + tenant_id=content_session.tenant_id, + ), + ) + content_path, content_type, content_name = ( + onlyoffice_service.get_rule_spreadsheet_content( + rule.id, + validated_session=content_session, + ) + ) + assert content_path.exists() + assert content_type == ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + ) + assert content_name.endswith(".xlsx") + with pytest.raises(AgentAssetOnlyOfficeSecurityError, match="不匹配"): service.validate_rule_spreadsheet_access_token( "different-asset", diff --git a/server/tests/test_agent_asset_tenant_security.py b/server/tests/test_agent_asset_tenant_security.py index b8db7c0..804f921 100644 --- a/server/tests/test_agent_asset_tenant_security.py +++ b/server/tests/test_agent_asset_tenant_security.py @@ -21,7 +21,7 @@ from app.models.agent_asset import AgentAsset, AgentAssetTestRun, AgentAssetVers from app.models.financial_record import ExpenseClaim from app.models.tenant import Tenant from app.schemas.agent_asset import AgentAssetRiskRuleScenarioTestRequest -from app.services.agent_asset_access import stable_user_principal +from app.services.agent_asset_access import AgentAssetAccessScope, stable_user_principal from app.services.agent_asset_onlyoffice_security import ( AgentAssetOnlyOfficeSessionService, ) @@ -173,6 +173,43 @@ def test_asset_reads_are_authenticated_and_tenant_scoped() -> None: assert anonymous.status_code == 401 +def test_platform_tenant_scope_requires_real_admin_and_is_platform_only() -> None: + non_admin = CurrentUserContext( + username="platform-manager", + name="平台经理", + role_codes=["manager"], + is_admin=False, + tenant_id="platform", + ) + with pytest.raises(PermissionError, match="有效租户"): + AgentAssetAccessScope.from_user(non_admin) + + platform_admin = CurrentUserContext( + username="platform-admin", + name="平台管理员", + role_codes=["manager"], + is_admin=True, + tenant_id="platform", + ) + access_scope = AgentAssetAccessScope.from_user(platform_admin) + + factory = _factory() + with factory() as db: + assets = _seed(db) + visible_assets = list( + db.scalars( + select(AgentAsset).where(access_scope.visibility_clause(AgentAsset)) + ).all() + ) + + assert [asset.id for asset in visible_assets] == [assets["platform"].id] + assert access_scope.can_write(assets["platform"]) is True + assert access_scope.can_write(assets["tenant-a"]) is False + access_scope.require_write(assets["platform"]) + with pytest.raises(LookupError, match="Asset not found"): + access_scope.require_write(assets["tenant-a"]) + + def test_version_write_uses_stable_principal_and_blocks_cross_tenant_or_platform() -> None: factory = _factory() with factory() as db: