diff --git a/backend/src/backend/resources.py b/backend/src/backend/resources.py index e57bcd0..92d5e6b 100644 --- a/backend/src/backend/resources.py +++ b/backend/src/backend/resources.py @@ -20,7 +20,7 @@ from common.storage.schemas import ( DownloadUrlRequest, ) from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request, status -from sqlalchemy import func, select +from sqlalchemy import func, or_, select from sqlalchemy.ext.asyncio import AsyncSession from backend.dependencies import ( @@ -148,10 +148,14 @@ def resource_payload( def can_view(resource: DataResources, context: RequestContext) -> bool: - # 2026-08-11: 同一 workspace 内成员可以查看彼此的资源(含 private); - # 保留 visibility 字段仅作为上传/绑定时的语义标签,不再影响读取。 - # 还原: 加回 owner_user_id 检查。 - return True + # 同一 workspace 内:owner 永远可见自己的资源(含 private); + # 其他成员只见 visibility in {workspace, public} 的资源; + # admin 全部可见。 + if resource.owner_user_id == context.user.user_id: + return True + if resource.visibility in {"workspace", "public"}: + return True + return context.is_admin # 根据当前脚本位置计算资源的相对路径,便于 Notebook 中用相对路径读取文件。 @@ -413,16 +417,16 @@ async def list_resources( escape="\\", ), ) - # 2026-08-11: 临时取消"用户间目录互相不可见"约束 - # 列表接口现在返回 workspace 内全部 active 资源(不再按 owner / visibility 过滤)。 - # 还原: 删除下面这段注释,恢复原来的 if not context.is_admin: ... 块。 - # if not context.is_admin: - # statement = statement.where( - # or_( - # DataResources.owner_user_id == context.user.user_id, - # DataResources.visibility.in_(["workspace", "public"]), - # ) - # ) + # 只返回 owner 自己的资源(含 private),或 visibility 为 + # workspace/public 的其他成员资源;A 的 private 资源对非 owner 不可见。 + # admin 跳过过滤,全部可见。 + if not context.is_admin: + statement = statement.where( + or_( + DataResources.owner_user_id == context.user.user_id, + DataResources.visibility.in_(["workspace", "public"]), + ) + ) if visibility: if visibility not in {"private", "workspace", "public"}: raise HTTPException( diff --git a/backend/tests/test_resources.py b/backend/tests/test_resources.py index 2bac1b9..22d3d58 100644 --- a/backend/tests/test_resources.py +++ b/backend/tests/test_resources.py @@ -420,13 +420,14 @@ def _resource_ctx(workspace_id: str = "W001") -> SimpleNamespace: request_id="test", user=SimpleNamespace(user_id="U001"), workspace=SimpleNamespace(workspace_id=workspace_id), + is_admin=False, ) -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.""" +class TestCanViewVisibility: + """同一 workspace 内:owner 永远可见自己的资源(含 private); + 其他成员只见 visibility in {workspace, public} 的资源; + admin 全部可见。与 list_resources 的 SQL 谓词一致。""" @staticmethod def _viewer() -> SimpleNamespace: @@ -437,9 +438,15 @@ class TestCanViewWorkspaceWideVisibility: is_admin=False, ) + @staticmethod + def _owner() -> SimpleNamespace: + ctx = TestCanViewVisibility._viewer() + ctx.user = SimpleNamespace(user_id="U001") # the owner + return ctx + @staticmethod def _admin() -> SimpleNamespace: - ctx = TestCanViewWorkspaceWideVisibility._viewer() + ctx = TestCanViewVisibility._viewer() ctx.is_admin = True return ctx @@ -449,13 +456,22 @@ class TestCanViewWorkspaceWideVisibility: res.visibility = visibility return res - def test_workspace_member_can_view_others_private_resource(self) -> None: + def test_owner_can_view_own_private_resource(self) -> None: + assert ( + can_view( + self._resource("private"), + self._owner(), + ) + is True + ) + + def test_workspace_member_cannot_view_others_private_resource(self) -> None: assert ( can_view( self._resource("private"), self._viewer(), ) - is True + is False ) def test_workspace_member_can_view_others_workspace_resource(self) -> None: