fix(resources): align can_view with list_resources (workspace-wide)
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。
This commit is contained in:
@@ -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 中用相对路径读取文件。
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user