fix(dashboard): skip unreadable plugin manifests
This commit is contained in:
@@ -540,21 +540,28 @@ def _discover_dashboard_plugins() -> list:
|
||||
plugins = []
|
||||
seen_names: set = set()
|
||||
for plugins_root, source in _dashboard_plugin_search_dirs():
|
||||
if not plugins_root.is_dir():
|
||||
try:
|
||||
if not plugins_root.is_dir():
|
||||
continue
|
||||
with os.scandir(plugins_root) as scan:
|
||||
children = sorted((Path(e.path) for e in scan), key=lambda p: p.name)
|
||||
except OSError as exc:
|
||||
_log.warning("Skipping unreadable dashboard plugin root %s: %s", plugins_root, exc)
|
||||
continue
|
||||
with os.scandir(plugins_root) as scan:
|
||||
children = sorted((Path(e.path) for e in scan), key=lambda p: p.name)
|
||||
for child in children:
|
||||
manifest_file = child / "dashboard" / "manifest.json"
|
||||
if not child.is_dir() or not manifest_file.exists():
|
||||
continue
|
||||
try:
|
||||
if not child.is_dir() or not manifest_file.exists():
|
||||
continue
|
||||
data = json.loads(manifest_file.read_text(encoding="utf-8"))
|
||||
name = data.get("name", child.name)
|
||||
if name in seen_names:
|
||||
continue
|
||||
seen_names.add(name)
|
||||
plugins.append(_dashboard_plugin_entry(data, name, child / "dashboard", source))
|
||||
except OSError as exc:
|
||||
_log.warning("Skipping unreadable dashboard plugin %s: %s", manifest_file, exc)
|
||||
continue
|
||||
except Exception as exc:
|
||||
_log.warning("Bad dashboard plugin manifest %s: %s", manifest_file, exc)
|
||||
continue
|
||||
|
||||
@@ -4540,6 +4540,51 @@ class TestDashboardPluginManifestExtensions:
|
||||
assert len(entries) == 1
|
||||
assert entries[0]["tab"]["path"] == "/from-profile"
|
||||
|
||||
def test_unreadable_plugin_paths_do_not_block_discovery(self, tmp_path, monkeypatch, caplog):
|
||||
"""A denied plugin directory or manifest must not prevent valid plugins loading."""
|
||||
from pathlib import Path
|
||||
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
self._write_plugin(tmp_path, "valid", {
|
||||
"name": "valid",
|
||||
"label": "Valid Plugin",
|
||||
"entry": "dist/index.js",
|
||||
})
|
||||
denied_root = tmp_path / "denied-root"
|
||||
denied_root.mkdir()
|
||||
denied_plugin = tmp_path / "plugins" / "denied"
|
||||
(denied_plugin / "dashboard").mkdir(parents=True)
|
||||
(denied_plugin / "dashboard" / "manifest.json").write_text("{}", encoding="utf-8")
|
||||
|
||||
from hermes_cli import web_server_dashboard
|
||||
original_search_dirs = web_server_dashboard._dashboard_plugin_search_dirs
|
||||
original_scandir = web_server_dashboard.os.scandir
|
||||
original_exists = Path.exists
|
||||
|
||||
def search_dirs():
|
||||
return [(denied_root, "user"), *original_search_dirs()]
|
||||
|
||||
def guarded_scandir(path):
|
||||
if Path(path) == denied_root:
|
||||
raise PermissionError("[WinError 5] Access is denied")
|
||||
return original_scandir(path)
|
||||
|
||||
def guarded_exists(path):
|
||||
if path == denied_plugin / "dashboard" / "manifest.json":
|
||||
raise PermissionError("[WinError 5] Access is denied")
|
||||
return original_exists(path)
|
||||
|
||||
monkeypatch.setattr(web_server_dashboard, "_dashboard_plugin_search_dirs", search_dirs)
|
||||
monkeypatch.setattr(web_server_dashboard.os, "scandir", guarded_scandir)
|
||||
monkeypatch.setattr(Path, "exists", guarded_exists)
|
||||
|
||||
plugins = web_server_dashboard._discover_dashboard_plugins()
|
||||
|
||||
assert "valid" in {plugin["name"] for plugin in plugins}
|
||||
assert "denied" not in {plugin["name"] for plugin in plugins}
|
||||
assert "Skipping unreadable dashboard plugin root" in caplog.text
|
||||
assert "Skipping unreadable dashboard plugin" in caplog.text
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user