fix(agent-assets): allow admin access to platform rules

This commit is contained in:
caoxiaozhu
2026-07-20 10:30:22 +08:00
parent 74fafc86d3
commit 140efd1a2a
4 changed files with 92 additions and 3 deletions

View File

@@ -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同时平台规则访问仍保持最小权限边界。

View File

@@ -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,

View File

@@ -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",

View File

@@ -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: