e93507a09c
Lockfile supply-chain audit / lockfile supply-chain audit (push) Has been cancelled
Windows Studio GGUF CI / GPU prebuilt resolves without Visual Studio (push) Has been cancelled
Windows Studio GGUF CI / setup.ps1 unit tests (VS 2026 / CMake guard) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2022) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-2025-vs2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-latest) (push) Has been cancelled
Windows Studio Update CI / Studio Updating Tests (push) Has been cancelled
Wheel CI / Wheel build + content sanity + import smoke (push) Has been cancelled
Lint CI / Source lint (Python + shell + YAML + JSON + safety nets) (push) Has been cancelled
MLX CI on Mac M1 / dispatch (push) Has been cancelled
Security audit / advisory audit (pip + npm + cargo) (push) Has been cancelled
Security audit / pip scan-packages :: extras (push) Has been cancelled
Security audit / pip scan-packages :: studio (push) Has been cancelled
Security audit / pip scan-packages :: hf-stack (push) Has been cancelled
Security audit / npm scan-packages (Studio frontend tarballs) (push) Has been cancelled
Security audit / workflow-trigger lint (pull_request_target / cache-poisoning) (push) Has been cancelled
Security audit / pytest tests/security (push) Has been cancelled
Security audit / npm provenance + new install-script diff (push) Has been cancelled
Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Backend CI / (Python 3.10) (push) Has been cancelled
Backend CI / (Python 3.11) (push) Has been cancelled
Backend CI / (Python 3.12) (push) Has been cancelled
Backend CI / (Python 3.13) (push) Has been cancelled
Backend CI / Repo tests (CPU) (push) Has been cancelled
Frontend CI / Frontend build + bundle sanity (push) Has been cancelled
Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Mac Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Mac Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-14) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15-intel) (push) Has been cancelled
Mac Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26-intel) (push) Has been cancelled
Mac Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Tauri CI / Tauri Linux debug build (no codesign) (push) Has been cancelled
Mac Studio Update CI / Studio Updating Tests (push) Has been cancelled
Studio UI CI / Chat UI Tests (push) Has been cancelled
Windows Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Windows Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Update CI / Studio Updating Tests (push) Has been cancelled
Core / Core (HF=default + TRL=default) (push) Has been cancelled
Core / Core (HF=4.57.6 + TRL<1) (push) Has been cancelled
Core / Core (HF=latest + TRL=latest) (push) Has been cancelled
Core / llama.cpp build + smoke (push) Has been cancelled
Windows Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Windows Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Windows Studio GGUF CI / JSON, images (push) Has been cancelled
Windows Studio GGUF CI / Studio install + inference without Visual Studio (push) Has been cancelled
Studio export capability / capability (macos-latest) (push) Has been cancelled
Studio export capability / capability (ubuntu-latest) (push) Has been cancelled
Studio export capability / capability (windows-latest) (push) Has been cancelled
Cross-platform parity / parity (macos-latest) (push) Has been cancelled
Cross-platform parity / parity (windows-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Studio load-orchestrator CI / test (push) Has been cancelled
288 lines
9.9 KiB
Python
288 lines
9.9 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-only
|
|
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
|
|
|
from __future__ import annotations
|
|
|
|
import ast
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from typing import Optional
|
|
|
|
import pytest
|
|
|
|
from hub.storage import scan_folders
|
|
from storage import studio_db
|
|
from utils.paths import external_media
|
|
|
|
|
|
_BACKEND_ROOT = Path(__file__).resolve().parent.parent
|
|
|
|
|
|
class _ExistingScanFolderConn:
|
|
def __init__(self):
|
|
self.params = ()
|
|
|
|
def execute(
|
|
self,
|
|
_sql,
|
|
params = (),
|
|
):
|
|
self.params = params
|
|
return self
|
|
|
|
def fetchone(self):
|
|
return {"id": 1, "path": self.params[0], "created_at": "fake"}
|
|
|
|
def commit(self):
|
|
pass
|
|
|
|
def close(self):
|
|
pass
|
|
|
|
|
|
class _HTTPException(Exception):
|
|
def __init__(self, status_code: int, detail: str):
|
|
super().__init__(detail)
|
|
self.status_code = status_code
|
|
self.detail = detail
|
|
|
|
|
|
def _stub_linux_path_checks(monkeypatch, module):
|
|
monkeypatch.setattr(module.platform, "system", lambda: "Linux")
|
|
monkeypatch.setattr(module.os.path, "realpath", os.path.normpath)
|
|
monkeypatch.setattr(module.os.path, "expanduser", lambda p: p)
|
|
monkeypatch.setattr(module.os.path, "exists", lambda _p: True)
|
|
monkeypatch.setattr(module.os.path, "isdir", lambda _p: True)
|
|
monkeypatch.setattr(module.os, "access", lambda _p, _mode: True)
|
|
|
|
|
|
def _stub_hub_scan_folder_db(monkeypatch):
|
|
monkeypatch.setattr(scan_folders, "_ensure_schema", lambda _conn: None)
|
|
monkeypatch.setattr(scan_folders, "get_connection", _ExistingScanFolderConn)
|
|
|
|
|
|
def _stub_legacy_scan_folder_db(monkeypatch):
|
|
monkeypatch.setattr(studio_db, "get_connection", _ExistingScanFolderConn)
|
|
|
|
|
|
def test_linux_run_media_policy_accepts_mounted_volume_descendants(monkeypatch):
|
|
monkeypatch.setattr(external_media.platform, "system", lambda: "Linux")
|
|
|
|
assert external_media.is_linux_run_media_path("/run/media/dspofu/nvmeB")
|
|
assert external_media.is_linux_run_media_path("/run/media/dspofu/nvmeB/modelsAI/gguf/qwen3.6")
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"path",
|
|
[
|
|
"/run",
|
|
"/run/media",
|
|
"/run/media/dspofu",
|
|
"/run/user/1000/models",
|
|
"/run/systemd/private",
|
|
"/run/not-media/dspofu/nvmeB",
|
|
],
|
|
)
|
|
def test_linux_run_media_policy_rejects_unrelated_run_paths(monkeypatch, path):
|
|
monkeypatch.setattr(external_media.platform, "system", lambda: "Linux")
|
|
|
|
assert not external_media.is_linux_run_media_path(path)
|
|
|
|
|
|
def test_linux_run_media_mount_roots_lists_readable_volume_roots(monkeypatch, tmp_path):
|
|
base = tmp_path / "run" / "media"
|
|
mount = base / "dspofu" / "nvmeB"
|
|
sensitive_mount = base / "dspofu" / ".ssh"
|
|
sensitive_aws_mount = base / "dspofu" / ".aws"
|
|
other_user_mount = base / "other" / "backup"
|
|
incomplete = base / "dspofu-only"
|
|
mount.mkdir(parents = True)
|
|
sensitive_mount.mkdir()
|
|
sensitive_aws_mount.mkdir()
|
|
other_user_mount.mkdir(parents = True)
|
|
incomplete.mkdir()
|
|
monkeypatch.setattr(external_media.platform, "system", lambda: "Linux")
|
|
|
|
roots = external_media.linux_run_media_mount_roots(base, user = "dspofu")
|
|
|
|
assert roots == [mount.resolve()]
|
|
|
|
|
|
def test_linux_run_media_mount_roots_skips_sensitive_resolved_volume_name(monkeypatch, tmp_path):
|
|
base = tmp_path / "run" / "media"
|
|
normal_mount = base / "dspofu" / "nvmeB"
|
|
sensitive_target = base / "dspofu" / ".config"
|
|
normal_mount.mkdir(parents = True)
|
|
sensitive_target.mkdir()
|
|
alias = base / "dspofu" / "config-alias"
|
|
alias.symlink_to(sensitive_target, target_is_directory = True)
|
|
monkeypatch.setattr(external_media.platform, "system", lambda: "Linux")
|
|
|
|
roots = external_media.linux_run_media_mount_roots(base, user = "dspofu")
|
|
|
|
assert roots == [normal_mount.resolve()]
|
|
|
|
|
|
def test_linux_run_media_mount_roots_skips_sensitive_resolved_descendant(monkeypatch, tmp_path):
|
|
base = tmp_path / "run" / "media"
|
|
normal_mount = base / "dspofu" / "nvmeB"
|
|
sensitive_descendant = normal_mount / ".ssh" / "models"
|
|
sensitive_descendant.mkdir(parents = True)
|
|
alias = base / "dspofu" / "models-alias"
|
|
alias.symlink_to(sensitive_descendant, target_is_directory = True)
|
|
monkeypatch.setattr(external_media.platform, "system", lambda: "Linux")
|
|
|
|
roots = external_media.linux_run_media_mount_roots(base, user = "dspofu")
|
|
|
|
assert roots == [normal_mount.resolve()]
|
|
|
|
|
|
def test_hub_scan_folder_accepts_linux_run_media_mount(monkeypatch):
|
|
_stub_linux_path_checks(monkeypatch, scan_folders)
|
|
monkeypatch.setattr(external_media.platform, "system", lambda: "Linux")
|
|
_stub_hub_scan_folder_db(monkeypatch)
|
|
target = "/run/media/dspofu/nvmeB/modelsAI/gguf/qwen3.6"
|
|
|
|
row = scan_folders.add_scan_folder(target)
|
|
|
|
assert row["path"] == target
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"target",
|
|
[
|
|
"/run",
|
|
"/run/media",
|
|
"/run/media/dspofu",
|
|
"/run/user/1000/models",
|
|
"/run/systemd/private",
|
|
"/run/not-media/dspofu/nvmeB",
|
|
],
|
|
)
|
|
def test_hub_scan_folder_keeps_unrelated_run_paths_blocked(monkeypatch, target):
|
|
_stub_linux_path_checks(monkeypatch, scan_folders)
|
|
monkeypatch.setattr(external_media.platform, "system", lambda: "Linux")
|
|
_stub_hub_scan_folder_db(monkeypatch)
|
|
|
|
with pytest.raises(ValueError, match = "Path under /run is not allowed"):
|
|
scan_folders.add_scan_folder(target)
|
|
|
|
|
|
def test_hub_scan_folder_keeps_sensitive_dirs_blocked_under_run_media(monkeypatch):
|
|
_stub_linux_path_checks(monkeypatch, scan_folders)
|
|
monkeypatch.setattr(external_media.platform, "system", lambda: "Linux")
|
|
_stub_hub_scan_folder_db(monkeypatch)
|
|
|
|
with pytest.raises(ValueError, match = "Credential or configuration"):
|
|
scan_folders.add_scan_folder("/run/media/dspofu/nvmeB/.ssh/models")
|
|
|
|
|
|
def test_legacy_scan_folder_accepts_linux_run_media_mount(monkeypatch):
|
|
_stub_linux_path_checks(monkeypatch, studio_db)
|
|
monkeypatch.setattr(external_media.platform, "system", lambda: "Linux")
|
|
_stub_legacy_scan_folder_db(monkeypatch)
|
|
target = "/run/media/dspofu/nvmeB/modelsAI/gguf/qwen3.6"
|
|
|
|
row = studio_db.add_scan_folder(target)
|
|
|
|
assert row["path"] == target
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"target",
|
|
[
|
|
"/run",
|
|
"/run/media",
|
|
"/run/media/dspofu",
|
|
"/run/user/1000/models",
|
|
"/run/systemd/private",
|
|
"/run/not-media/dspofu/nvmeB",
|
|
],
|
|
)
|
|
def test_legacy_scan_folder_keeps_unrelated_run_paths_blocked(monkeypatch, target):
|
|
_stub_linux_path_checks(monkeypatch, studio_db)
|
|
monkeypatch.setattr(external_media.platform, "system", lambda: "Linux")
|
|
_stub_legacy_scan_folder_db(monkeypatch)
|
|
|
|
with pytest.raises(ValueError, match = "Path under /run is not allowed"):
|
|
studio_db.add_scan_folder(target)
|
|
|
|
|
|
def test_legacy_scan_folder_keeps_sensitive_dirs_blocked_under_run_media(monkeypatch):
|
|
_stub_linux_path_checks(monkeypatch, studio_db)
|
|
monkeypatch.setattr(external_media.platform, "system", lambda: "Linux")
|
|
_stub_legacy_scan_folder_db(monkeypatch)
|
|
|
|
with pytest.raises(ValueError, match = "Credential or configuration"):
|
|
studio_db.add_scan_folder("/run/media/dspofu/nvmeB/.aws/models")
|
|
|
|
|
|
def test_legacy_browse_allowlist_includes_linux_run_media_mounts(monkeypatch, tmp_path):
|
|
tree = ast.parse((_BACKEND_ROOT / "routes" / "models.py").read_text(encoding = "utf-8"))
|
|
function_names = {
|
|
"_build_browse_allowlist",
|
|
"_browse_relative_parts",
|
|
"_is_path_inside_allowlist",
|
|
"_match_browse_child",
|
|
"_normalize_browse_request_path",
|
|
"_resolve_browse_target",
|
|
}
|
|
functions = [
|
|
node
|
|
for node in tree.body
|
|
if isinstance(node, ast.FunctionDef) and node.name in function_names
|
|
]
|
|
module = ast.Module(body = functions, type_ignores = [])
|
|
ast.fix_missing_locations(module)
|
|
|
|
home = tmp_path / "home"
|
|
media_root = tmp_path / "run" / "media" / "dspofu" / "nvmeB"
|
|
model_dir = media_root / "modelsAI" / "gguf" / "qwen3.6"
|
|
home.mkdir()
|
|
model_dir.mkdir(parents = True)
|
|
(media_root / ".ssh").mkdir()
|
|
|
|
fake_paths = SimpleNamespace(
|
|
hf_default_cache_dir = lambda: tmp_path / "missing-default-hf",
|
|
legacy_hf_cache_dir = lambda: tmp_path / "missing-legacy-hf",
|
|
well_known_model_dirs = lambda: [],
|
|
studio_root = lambda: tmp_path / "missing-studio",
|
|
outputs_root = lambda: tmp_path / "missing-outputs",
|
|
exports_root = lambda: tmp_path / "missing-exports",
|
|
)
|
|
fake_external_media = SimpleNamespace(linux_run_media_mount_roots = lambda: [media_root])
|
|
fake_studio_db = SimpleNamespace(
|
|
list_scan_folders = lambda: [],
|
|
contains_sensitive_path_component = studio_db.contains_sensitive_path_component,
|
|
)
|
|
monkeypatch.setitem(sys.modules, "utils.paths", fake_paths)
|
|
monkeypatch.setitem(sys.modules, "utils.paths.external_media", fake_external_media)
|
|
monkeypatch.setitem(sys.modules, "storage.studio_db", fake_studio_db)
|
|
|
|
ns = {
|
|
"HTTPException": _HTTPException,
|
|
"os": os,
|
|
"Path": Path,
|
|
"Optional": Optional,
|
|
"_safe_is_dir": lambda p: Path(p).is_dir(),
|
|
"_resolve_hf_cache_dir": lambda: tmp_path / "missing-hf",
|
|
"logger": SimpleNamespace(debug = lambda *_args, **_kwargs: None),
|
|
}
|
|
exec(compile(module, "<extracted routes/models.py>", "exec"), ns)
|
|
|
|
allowlist = ns["_build_browse_allowlist"]()
|
|
|
|
assert media_root.resolve() in allowlist
|
|
assert ns["_resolve_browse_target"](str(model_dir), allowlist) == model_dir.resolve()
|
|
|
|
with pytest.raises(_HTTPException) as exc:
|
|
ns["_resolve_browse_target"](str(media_root / ".ssh"), allowlist)
|
|
assert exc.value.status_code == 403
|
|
|
|
ssh_root = media_root / ".ssh"
|
|
with pytest.raises(_HTTPException) as exc_root:
|
|
ns["_resolve_browse_target"](str(ssh_root), [ssh_root])
|
|
assert exc_root.value.status_code == 403
|