From 5483d19dbdb2af0d7c73b85c7801d9f920bfdbbb Mon Sep 17 00:00:00 2001 From: "tao.chen" <93983997+taochen-ct@users.noreply.github.com> Date: Fri, 21 Aug 2026 12:40:27 +0800 Subject: [PATCH] fix(resources): align can_view with list_resources (workspace-wide) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit list_resources 已放开成 workspace-wide,但 can_view 仍要求 visibility in {workspace, public} 或 admin,导致 A 的 private 资源出现在 B 的列表里、但 B 调 GET /{id}、POST /{id}/download-url 与 POST /{id}/jupyter-relative-path 全部 404。 can_view 改为恒真,访问边界全部交给 get_visible_resource 的 workspace_id + status=active SQL 限定;visibility 字段保留为 上传/绑定时的语义标签。delete_resource 仍按 owner/admin 校验 403,不受影响。补 4 个 unit test。 --- backend/src/backend/resources.py | 13 ++----- backend/tests/test_resources.py | 64 ++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 9 deletions(-) diff --git a/backend/src/backend/resources.py b/backend/src/backend/resources.py index 950a052..e57bcd0 100644 --- a/backend/src/backend/resources.py +++ b/backend/src/backend/resources.py @@ -148,15 +148,10 @@ def resource_payload( def can_view(resource: DataResources, context: RequestContext) -> bool: - # 2026-08-11: 临时取消"用户间目录互相不可见"约束 - # 同一 workspace 内的成员现在可以查看彼此的 private 资源。 - # 还原: 取消下方注释,恢复 owner_user_id 检查。 - return ( - # resource.owner_user_id == context.user.user_id - # or - resource.visibility in {"workspace", "public"} - or context.is_admin - ) + # 2026-08-11: 同一 workspace 内成员可以查看彼此的资源(含 private); + # 保留 visibility 字段仅作为上传/绑定时的语义标签,不再影响读取。 + # 还原: 加回 owner_user_id 检查。 + return True # 根据当前脚本位置计算资源的相对路径,便于 Notebook 中用相对路径读取文件。 diff --git a/backend/tests/test_resources.py b/backend/tests/test_resources.py index 30566a1..2bac1b9 100644 --- a/backend/tests/test_resources.py +++ b/backend/tests/test_resources.py @@ -15,6 +15,7 @@ from sqlalchemy import Column, MetaData, String, Table, create_engine, select from sqlalchemy.dialects import mysql as mysql_dialect from backend.resources import ( _build_list_resources_descendant_prefix, + can_view, compute_jupyter_relative_path, resource_directory, resource_payload, @@ -422,6 +423,69 @@ def _resource_ctx(workspace_id: str = "W001") -> SimpleNamespace: ) +class TestCanViewWorkspaceWideVisibility: + """can_view is workspace-wide: any workspace member may view any active + resource, matching list_resources since 2026-08-11. visibility is only a + semantic tag on upload/bind and no longer gates reads.""" + + @staticmethod + def _viewer() -> SimpleNamespace: + return SimpleNamespace( + request_id="test", + user=SimpleNamespace(user_id="U002"), # not the owner + workspace=SimpleNamespace(workspace_id="W001"), + is_admin=False, + ) + + @staticmethod + def _admin() -> SimpleNamespace: + ctx = TestCanViewWorkspaceWideVisibility._viewer() + ctx.is_admin = True + return ctx + + @staticmethod + def _resource(visibility: str) -> SimpleNamespace: + res = _make_resource("W001", "U001") + res.visibility = visibility + return res + + def test_workspace_member_can_view_others_private_resource(self) -> None: + assert ( + can_view( + self._resource("private"), + self._viewer(), + ) + is True + ) + + def test_workspace_member_can_view_others_workspace_resource(self) -> None: + assert ( + can_view( + self._resource("workspace"), + self._viewer(), + ) + is True + ) + + def test_workspace_member_can_view_others_public_resource(self) -> None: + assert ( + can_view( + self._resource("public"), + self._viewer(), + ) + is True + ) + + def test_admin_can_view_others_private_resource(self) -> None: + assert ( + can_view( + self._resource("private"), + self._admin(), + ) + is True + ) + + class TestBuildListResourcesDescendantPrefix: """Pure-function contract for the escaped object_key prefix. Unlike the scripts helper there is NO user scope — data resources are