Develop #41

Merged
tao.chen merged 96 commits from develop into main 2026-09-02 10:15:06 +08:00
2 changed files with 87 additions and 9 deletions
Showing only changes of commit 9233d99237 - Show all commits
+8 -8
View File
@@ -704,7 +704,7 @@ async def get_workspace_tree(
StorageObjects.workspace_id == context.workspace.workspace_id,
StorageObjects.object_status == "available",
StorageObjects.is_deleted == 0,
StorageObjects.relative_path.like(like_prefix),
StorageObjects.relative_path.like(like_prefix, escape="\\"),
StorageObjects.usage_type.notin_(TREE_EXCLUDED_USAGE_TYPES),
)
)
@@ -777,8 +777,8 @@ async def list_workspace_directories(
StorageObjects.workspace_id == context.workspace.workspace_id,
StorageObjects.object_status == "available",
StorageObjects.is_deleted == 0,
StorageObjects.relative_path.like(f"{descendant_prefix}%"),
~StorageObjects.relative_path.like(f"{descendant_prefix}%/%"),
StorageObjects.relative_path.like(f"{descendant_prefix}%", escape="\\"),
~StorageObjects.relative_path.like(f"{descendant_prefix}%/%", escape="\\"),
StorageObjects.object_type == "directory",
StorageObjects.usage_type.notin_(TREE_EXCLUDED_USAGE_TYPES),
)
@@ -811,8 +811,8 @@ async def list_workspace_directories(
StorageObjects.workspace_id == context.workspace.workspace_id,
StorageObjects.object_status == "available",
StorageObjects.is_deleted == 0,
StorageObjects.relative_path.like(f"{child_prefix}%"),
~StorageObjects.relative_path.like(f"{child_prefix}%/%"),
StorageObjects.relative_path.like(f"{child_prefix}%", escape="\\"),
~StorageObjects.relative_path.like(f"{child_prefix}%/%", escape="\\"),
StorageObjects.usage_type.notin_(TREE_EXCLUDED_USAGE_TYPES),
).limit(1)
)
@@ -1014,7 +1014,7 @@ async def delete_workspace_directory(
select(StorageObjects).where(
StorageObjects.workspace_id == context.workspace.workspace_id,
StorageObjects.object_status == "available",
StorageObjects.relative_path.like(f"{child_prefix}%"),
StorageObjects.relative_path.like(f"{child_prefix}%", escape="\\"),
).order_by(func.length(StorageObjects.relative_path).desc())
)
)
@@ -1098,8 +1098,8 @@ async def list_scripts(
.where(
Scripts.workspace_id == context.workspace.workspace_id,
Scripts.status == "active",
StorageObjects.relative_path.like(f"{descendant_prefix}%"),
~StorageObjects.relative_path.like(f"{descendant_prefix}%/%"),
StorageObjects.relative_path.like(f"{descendant_prefix}%", escape="\\"),
~StorageObjects.relative_path.like(f"{descendant_prefix}%/%", escape="\\"),
)
.order_by(Scripts.updated_at.desc())
)
+79 -1
View File
@@ -135,4 +135,82 @@ async def test_list_scripts_empty_parent_path_targets_root_descendants() -> None
sql = captured_sql[0].lower()
assert "like 'workspace/alice/%%'" in sql
assert "not like 'workspace/alice/%%/%%'" in sql
assert "not like 'workspace/alice/%%/%%'" in sql
async def test_list_scripts_where_clause_includes_escape() -> None:
"""Both LIKE clauses must declare ESCAPE so folder names containing ``_``
or ``%`` do not act as SQL wildcards and match sibling paths."""
from backend.scripts import list_scripts
captured_sql: list[str] = []
class _MockResult:
def all(self):
return []
mock_session = MagicMock()
mock_session.execute = AsyncMock(
side_effect=lambda stmt: (
captured_sql.append(
str(
stmt.compile(
dialect=mysql_dialect.dialect(),
compile_kwargs={"literal_binds": True},
)
)
)
or _MockResult()
)
)
await list_scripts(parent_path="foo_bar", context=_ctx("alice"), session=mock_session)
sql = captured_sql[0]
# Both patterns must carry ESCAPE; counts must match between the two LIKE
# occurrences (one positive, one negated). SQLAlchemy doubles the escape
# char for SQL string literals, so the rendered form is `ESCAPE '\\\\'`.
assert sql.count("ESCAPE '\\\\'") == 2, sql
async def test_list_workspace_directories_where_clause_includes_escape() -> None:
"""list_workspace_directories must also emit ESCAPE — the same LIKE
pattern was already vulnerable for pre-existing endpoints; this
endpoint is in scope for the same fix."""
from backend.scripts import list_workspace_directories
captured_sql: list[str] = []
class _MockScalarResult:
def scalar(self):
return None
class _MockResult:
def all(self):
return []
def scalars(self):
return _MockScalarResult()
mock_session = MagicMock()
mock_session.execute = AsyncMock(
side_effect=lambda stmt: (
captured_sql.append(
str(
stmt.compile(
dialect=mysql_dialect.dialect(),
compile_kwargs={"literal_binds": True},
)
)
)
or _MockResult()
)
)
await list_workspace_directories(
parent_path="foo_bar", context=_ctx("alice"), session=mock_session
)
# Two LIKE clauses in the children query + two in the has_children
# check per directory in the result — for an empty result set only
# the first batch executes, so we expect at least 2 ESCAPEs.
sql = " ".join(captured_sql)
assert sql.count("ESCAPE '\\\\'") >= 2, sql