fix(scripts): workspace-wide parent_path listing + private visibility on reads
1. 同 workspace 互相可见(排除 private):
- list_scripts / count_scripts 已 workspace-wide + visibility 过滤,但
单条读取(get/content/latest-version/versions)不校验 visibility,非
owner 猜 id 即可读他人 private 脚本。新增 script_can_view(与 data
resources 的 can_view 对称)并在 get_script_row / latest_version 强制,
private 对非 owner 返回 404。
2. parent_path 为空默认拉根路径文件:
- 物理存储为 workspace/{user_id}/...,根 prefix 原来是 workspace/ +
NOT LIKE workspace/%/%,所有文件都在两层被整体排除,list_scripts("")
恒空。改为 workspace/%/,配合 LIKE workspace/%/% AND NOT LIKE
workspace/%/%/% 返回各 owner 根级文件。
3. 非空 parent_path 跨 owner 查询:
- 原来 workspace/foo/ 永远匹配不到 workspace/{uid}/foo/...,子目录
懒加载返回空,其他用户目录点击无内容。改为 workspace/%/foo/(owner
段通配,与 list_resources 一致),_ / % 仍按字面转义。
测试:更新前缀契约断言,新增 SQLite 行为测试(跨 owner 根/子目录、转义)
与 script_can_view / get_script 权限测试,133 passed。
This commit is contained in:
@@ -68,10 +68,11 @@ async def test_count_scripts_returns_scalar_int() -> None:
|
||||
# JOIN to StorageObjects so orphaned scripts (no joinable row) are
|
||||
# excluded — matches list_scripts INNER JOIN behaviour.
|
||||
assert "inner join storage_objects" in sql
|
||||
# Scope: workspace_id + active status + workspace-wide prefix.
|
||||
# Scope: workspace_id + active status + workspace-wide prefix
|
||||
# (owner segment wildcarded: workspace/%/%).
|
||||
assert "scripts.workspace_id" in sql
|
||||
assert "scripts.status" in sql
|
||||
assert "like 'workspace/%%'" in sql
|
||||
assert "like 'workspace/%%/%%'" in sql
|
||||
# Non-admin (default) narrows by visibility.
|
||||
assert "scripts.owner_user_id = 'u001'" in sql
|
||||
assert "scripts.visibility in ('workspace', 'public')" in sql
|
||||
@@ -87,9 +88,10 @@ async def test_count_scripts_handles_null_result() -> None:
|
||||
|
||||
|
||||
async def test_count_scripts_workspace_wide_not_user_scoped() -> None:
|
||||
"""The prefix is workspace-wide (``workspace/%`` — no embedded user_id),
|
||||
so different users count the same physical tree; the only per-user
|
||||
difference is the non-admin visibility predicate (owner_user_id = me)."""
|
||||
"""The prefix is workspace-wide (``workspace/%/%`` — owner segment
|
||||
wildcarded, no embedded user_id), so different users count the same
|
||||
physical tree; the only per-user difference is the non-admin
|
||||
visibility predicate (owner_user_id = me)."""
|
||||
captured = []
|
||||
|
||||
mock_session = MagicMock()
|
||||
@@ -104,8 +106,8 @@ async def test_count_scripts_workspace_wide_not_user_scoped() -> None:
|
||||
sql_bob = _compile(captured[-1]).lower()
|
||||
|
||||
# Both count the same workspace-wide subtree.
|
||||
assert "like 'workspace/%%'" in sql_alice
|
||||
assert "like 'workspace/%%'" in sql_bob
|
||||
assert "like 'workspace/%%/%%'" in sql_alice
|
||||
assert "like 'workspace/%%/%%'" in sql_bob
|
||||
# Neither embeds the user_id in the path prefix.
|
||||
assert "workspace/alice/%" not in sql_alice
|
||||
assert "workspace/bob/%" not in sql_bob
|
||||
@@ -129,7 +131,7 @@ async def test_count_scripts_admin_skips_visibility_filter() -> None:
|
||||
)
|
||||
assert result["data"] == {"total": 42}
|
||||
sql = _compile(captured[0]).lower()
|
||||
assert "like 'workspace/%%'" in sql
|
||||
assert "like 'workspace/%%/%%'" in sql
|
||||
# visibility / owner_user_id still appear in the SELECT projection, but
|
||||
# the visibility WHERE predicate must be absent for admins.
|
||||
assert "scripts.visibility in ('workspace', 'public')" not in sql
|
||||
|
||||
Reference in New Issue
Block a user