mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-05-22 16:06:50 +00:00
fix(sandbox): add group/other read permissions to uploaded files for Docker sandbox (#3127)
When using AIO sandbox with LocalContainerBackend, uploaded files are created with 0o600 (owner-only) permissions by the gateway process running as root. The sandbox process inside the Docker container runs as a non-root user and cannot read these bind-mounted files, causing a "Permission denied" error on read_file. Add `needs_upload_permission_adjustment` attribute to SandboxProvider (default True) to indicate that uploaded files need chmod adjustment. LocalSandboxProvider opts out (same user). A new `_make_file_sandbox_readable` function adds S_IRGRP | S_IROTH bits after files are written, changing permissions from 0o600 to 0o644 so the sandbox can read the uploads. fixes #3127
This commit is contained in:
@@ -218,6 +218,7 @@ def test_upload_files_does_not_adjust_permissions_for_local_sandbox(tmp_path):
|
||||
|
||||
provider = MagicMock()
|
||||
provider.uses_thread_data_mounts = True
|
||||
provider.needs_upload_permission_adjustment = False
|
||||
provider.acquire.return_value = "local"
|
||||
sandbox = MagicMock()
|
||||
provider.get.return_value = sandbox
|
||||
@@ -227,12 +228,14 @@ def test_upload_files_does_not_adjust_permissions_for_local_sandbox(tmp_path):
|
||||
patch.object(uploads, "ensure_uploads_dir", return_value=thread_uploads_dir),
|
||||
patch.object(uploads, "get_sandbox_provider", return_value=provider),
|
||||
patch.object(uploads, "_make_file_sandbox_writable") as make_writable,
|
||||
patch.object(uploads, "_make_file_sandbox_readable") as make_readable,
|
||||
):
|
||||
file = UploadFile(filename="notes.txt", file=BytesIO(b"hello uploads"))
|
||||
result = asyncio.run(call_unwrapped(uploads.upload_files, "thread-local", request=MagicMock(), files=[file], config=SimpleNamespace()))
|
||||
|
||||
assert result.success is True
|
||||
make_writable.assert_not_called()
|
||||
make_readable.assert_not_called()
|
||||
|
||||
|
||||
def test_upload_files_acquires_non_local_sandbox_before_writing(tmp_path):
|
||||
@@ -430,6 +433,59 @@ def test_make_file_sandbox_writable_skips_symlinks(tmp_path):
|
||||
chmod.assert_not_called()
|
||||
|
||||
|
||||
def test_make_file_sandbox_readable_adds_read_bits_for_regular_files(tmp_path):
|
||||
file_path = tmp_path / "data.csv"
|
||||
file_path.write_bytes(b"csv-data")
|
||||
# Simulate the 0o600 permissions set by open_upload_file_no_symlink
|
||||
file_path.chmod(0o600)
|
||||
|
||||
uploads._make_file_sandbox_readable(file_path)
|
||||
|
||||
updated_mode = stat.S_IMODE(file_path.stat().st_mode)
|
||||
assert updated_mode & stat.S_IRUSR
|
||||
assert updated_mode & stat.S_IRGRP
|
||||
assert updated_mode & stat.S_IROTH
|
||||
|
||||
|
||||
def test_make_file_sandbox_readable_skips_symlinks(tmp_path):
|
||||
file_path = tmp_path / "target-link.txt"
|
||||
file_path.write_text("hello", encoding="utf-8")
|
||||
symlink_stat = MagicMock(st_mode=stat.S_IFLNK)
|
||||
|
||||
with (
|
||||
patch.object(uploads.os, "lstat", return_value=symlink_stat),
|
||||
patch.object(uploads.os, "chmod") as chmod,
|
||||
):
|
||||
uploads._make_file_sandbox_readable(file_path)
|
||||
|
||||
chmod.assert_not_called()
|
||||
|
||||
|
||||
def test_upload_files_adjusts_read_permissions_for_mounted_non_local_sandbox(tmp_path):
|
||||
thread_uploads_dir = tmp_path / "uploads"
|
||||
thread_uploads_dir.mkdir(parents=True)
|
||||
|
||||
# AIO sandbox with LocalContainerBackend: uses_thread_data_mounts=True
|
||||
# but needs_upload_permission_adjustment=True (default)
|
||||
provider = MagicMock()
|
||||
provider.uses_thread_data_mounts = True
|
||||
provider.needs_upload_permission_adjustment = True
|
||||
|
||||
with (
|
||||
patch.object(uploads, "get_uploads_dir", return_value=thread_uploads_dir),
|
||||
patch.object(uploads, "ensure_uploads_dir", return_value=thread_uploads_dir),
|
||||
patch.object(uploads, "get_sandbox_provider", return_value=provider),
|
||||
patch.object(uploads, "_make_file_sandbox_readable") as make_readable,
|
||||
):
|
||||
file = UploadFile(filename="notes.txt", file=BytesIO(b"hello uploads"))
|
||||
result = asyncio.run(call_unwrapped(uploads.upload_files, "thread-aio", request=MagicMock(), files=[file], config=SimpleNamespace()))
|
||||
|
||||
assert result.success is True
|
||||
make_readable.assert_called_once()
|
||||
called_path = make_readable.call_args[0][0]
|
||||
assert called_path.name == "notes.txt"
|
||||
|
||||
|
||||
def test_upload_files_rejects_dotdot_and_dot_filenames(tmp_path):
|
||||
thread_uploads_dir = tmp_path / "uploads"
|
||||
thread_uploads_dir.mkdir(parents=True)
|
||||
|
||||
Reference in New Issue
Block a user