Path.exists/is_dir swallow PermissionError into False on 3.12+, so a stat patch never reached the loader's except branch.
38 lines
1.5 KiB
Python
38 lines
1.5 KiB
Python
"""One mode-000 / ACL-denied child under a plugin root must not abort the listing (#111804)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from plugins import plugin_loader
|
|
|
|
|
|
def _deny(monkeypatch, denied: Path) -> None:
|
|
"""chmod 000 does not bite as root, so fail the probes of the denied child's ``__init__.py``.
|
|
|
|
``Path.exists``/``is_dir`` route through ``os.path`` on 3.12+, which swallows
|
|
``PermissionError`` into False; the loader's own guard sees the error only when the
|
|
pathlib probe raises, so fail both layers the way a real ACL denial can."""
|
|
real_exists, real_is_dir = Path.exists, Path.is_dir
|
|
|
|
def refuse(method):
|
|
def probe(self, *args, **kwargs):
|
|
if self.parent == denied or self == denied:
|
|
raise PermissionError(13, "Permission denied", str(self))
|
|
return method(self, *args, **kwargs)
|
|
return probe
|
|
|
|
monkeypatch.setattr(Path, "exists", refuse(real_exists))
|
|
monkeypatch.setattr(Path, "is_dir", refuse(real_is_dir))
|
|
|
|
|
|
def test_iter_plugin_dirs_skips_unreadable_child(tmp_path, monkeypatch, caplog):
|
|
for name in ("denied", "good"):
|
|
(tmp_path / name).mkdir()
|
|
(tmp_path / name / "__init__.py").write_text("", encoding="utf-8")
|
|
_deny(monkeypatch, tmp_path / "denied")
|
|
|
|
with caplog.at_level("WARNING", logger="plugins.plugin_loader"):
|
|
assert plugin_loader.iter_plugin_dirs(tmp_path) == [tmp_path / "good"]
|
|
assert "Skipping unreadable plugin directory" in caplog.text
|