mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-29 11:12:29 +00:00
fix(profiles): refuse lifecycle change on protected modules + fsync manifest (ref #896)
Review follow-ups for the awake-level setter: - reject a protected module server-side (web 409) and in the ctl (refused, reason=protected) — it is forced always-on anyway, so the write is spurious and would dirty a core manifest; mirrors set_pin's protected refusal. - fsync manifest_edit's atomic write before rename (the manifest is source of truth, not a regenerable derived file) — parity with web._atomic_write. - tests: protected refusal (web + cli), duplicate-same-key collapse, manifest with no trailing newline. Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
This commit is contained in:
parent
c1970decc9
commit
6e4591e397
|
|
@ -341,9 +341,17 @@ def _cmd_set_lifecycle(args) -> int:
|
|||
# lifecycle/wake_class sont déjà bornés par argparse (choices=) + revalidés
|
||||
# dans manifest_edit.set_lifecycle ; ici seul le module (positionnel libre)
|
||||
# peut être inconnu.
|
||||
if args.module not in manifests:
|
||||
refused = {"status": "refused", "module": args.module, "reason": "unknown"}
|
||||
print(json.dumps(refused) if args.json else "refusé: module inconnu",
|
||||
m0 = manifests.get(args.module)
|
||||
reason = None
|
||||
if m0 is None:
|
||||
reason = "unknown"
|
||||
elif m0.protected:
|
||||
# Défense en profondeur (le panel refuse déjà 409) : ne pas réécrire le
|
||||
# manifeste d'un module protégé, forcé always-on de toute façon.
|
||||
reason = "protected"
|
||||
if reason is not None:
|
||||
refused = {"status": "refused", "module": args.module, "reason": reason}
|
||||
print(json.dumps(refused) if args.json else f"refusé: {reason}",
|
||||
file=sys.stdout if args.json else sys.stderr)
|
||||
return 3
|
||||
|
||||
|
|
|
|||
|
|
@ -46,6 +46,11 @@ def _write_atomic(path: Path, text: str) -> None:
|
|||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
||||
f.write(text)
|
||||
# Le manifeste est source de vérité (pas un dérivé régénérable) : on
|
||||
# fsync avant le rename, comme web._atomic_write, pour qu'une coupure
|
||||
# ne puisse pas laisser un manifeste vide/tronqué.
|
||||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
os.chmod(tmp, stat.S_IMODE(orig_mode))
|
||||
os.replace(tmp, path)
|
||||
except BaseException:
|
||||
|
|
|
|||
|
|
@ -651,8 +651,16 @@ def create_app() -> FastAPI:
|
|||
root = _root()
|
||||
mod_dir, _prof_dir, _pins_file, _active = _cli._paths(root)
|
||||
manifests = _load_manifests_or_500(mod_dir)
|
||||
if body.module not in manifests:
|
||||
m = manifests.get(body.module)
|
||||
if m is None:
|
||||
raise HTTPException(status_code=404, detail=f"module inconnu: {body.module}")
|
||||
# Refus STRUCTUREL comme set_pin : un module protégé est forcé always-on
|
||||
# (effective_lifecycle), fixer son niveau d'éveil n'aurait aucun effet et
|
||||
# ne ferait que salir un manifeste du cœur — jamais de sudo pour ça.
|
||||
if m.protected:
|
||||
raise HTTPException(status_code=409,
|
||||
detail=f"{body.module} est protégé (toujours actif) — "
|
||||
"niveau d'éveil non modifiable")
|
||||
async with _apply_lock:
|
||||
return await _run_setlifecycle_ctl(body.module, body.lifecycle, body.wake_class)
|
||||
|
||||
|
|
|
|||
|
|
@ -544,3 +544,15 @@ def test_set_lifecycle_rejects_bad_enum_via_argparse(lc_root):
|
|||
with pytest.raises(SystemExit):
|
||||
main(["--root", str(lc_root), "set-lifecycle", "yacy",
|
||||
"--lifecycle", "turbo", "--wake-class", "normal", "--json"])
|
||||
|
||||
|
||||
def test_set_lifecycle_protected_module_refused(lc_root, capsys):
|
||||
(lc_root / "modules.d" / "auth.toml").write_text(
|
||||
'id="auth"\ncategory="security"\nruntime="native"\nexposure="lan"\n'
|
||||
'units=["secubox-auth.service"]\nlifecycle="always-on"\n')
|
||||
rc = main(["--root", str(lc_root), "set-lifecycle", "auth",
|
||||
"--lifecycle", "on-demand", "--wake-class", "normal", "--json"])
|
||||
out = json.loads(capsys.readouterr().out)
|
||||
assert rc == 3 and out["status"] == "refused" and out["reason"] == "protected"
|
||||
# manifest NOT rewritten
|
||||
assert 'lifecycle="always-on"' in (lc_root / "modules.d" / "auth.toml").read_text()
|
||||
|
|
|
|||
|
|
@ -100,3 +100,25 @@ def test_preserves_file_mode(tmp_path):
|
|||
p.chmod(0o644)
|
||||
set_lifecycle(p, lifecycle="eager", wake_class="normal")
|
||||
assert stat.S_IMODE(p.stat().st_mode) == 0o644
|
||||
|
||||
|
||||
def test_collapses_duplicate_same_key(tmp_path):
|
||||
from api.manifest_edit import set_lifecycle
|
||||
p = tmp_path / "m.toml"
|
||||
p.write_text('id="m"\ncategory="infra"\nruntime="native"\nexposure="lan"\nunits=["m.service"]\n'
|
||||
'lifecycle = "manual"\nlifecycle = "eager"\n')
|
||||
set_lifecycle(p, lifecycle="on-demand", wake_class="normal")
|
||||
assert p.read_text().count("lifecycle") == 1
|
||||
m = _load(p)
|
||||
assert m.lifecycle == "on-demand"
|
||||
|
||||
|
||||
def test_handles_no_trailing_newline(tmp_path):
|
||||
from api.manifest_edit import set_lifecycle
|
||||
p = tmp_path / "m.toml"
|
||||
p.write_text('id="m"\ncategory="infra"\nruntime="native"\nexposure="lan"\nunits=["m.service"]\n'
|
||||
'lifecycle = "manual"') # no trailing newline
|
||||
set_lifecycle(p, lifecycle="eager", wake_class="urgent")
|
||||
m = _load(p)
|
||||
assert m.lifecycle == "eager" and m.wake_class == "urgent"
|
||||
assert p.read_text().endswith("\n")
|
||||
|
|
|
|||
|
|
@ -740,3 +740,12 @@ def test_lifecycle_route_unknown_module_404_and_ctl_not_called(client, monkeypat
|
|||
json={"module": "fantome", "lifecycle": "on-demand", "wake_class": "normal"})
|
||||
assert r.status_code == 404
|
||||
assert called == []
|
||||
|
||||
|
||||
def test_lifecycle_route_protected_409_and_ctl_not_called(client, monkeypatch):
|
||||
called = []
|
||||
monkeypatch.setattr(web, "_ctl_run", lambda argv, **kw: called.append(argv))
|
||||
r = client.post("/api/v1/profiles/lifecycle",
|
||||
json={"module": "auth", "lifecycle": "on-demand", "wake_class": "normal"})
|
||||
assert r.status_code == 409
|
||||
assert called == []
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user