From 6dfc5a5ff05c1f070fcc152b7585ee453be4d032 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Sat, 25 Jul 2026 14:40:35 +0200 Subject: [PATCH] fix(annuaire): standing-mode assist request now requires an active assist grant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit can_open() accepted mode=="standing" unconditionally, letting a center open a session on the strength of the request alone. Per spec, standing mode is a persistent authorization and must be backed by a real, self-issued delegation: can_open() now additionally requires an active, SELF-issued (issued_by == self_did) capability="assist" Grant naming that request's center_did, via grants.active_grants() — the sovereignty filter is already enforced there. Missing grant -> (False, "no-standing-grant"). Per-incident mode is unchanged: the box-authored request remains its own authority. ref sous-projet 2 Co-Authored-By: Gerald KERMA --- packages/secubox-annuaire/annuaire/assist.py | 22 ++++++- .../tests/test_assist_resolve.py | 60 +++++++++++++++++++ 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/packages/secubox-annuaire/annuaire/assist.py b/packages/secubox-annuaire/annuaire/assist.py index 14b21072..b078d1fa 100644 --- a/packages/secubox-annuaire/annuaire/assist.py +++ b/packages/secubox-annuaire/annuaire/assist.py @@ -14,7 +14,7 @@ from __future__ import annotations from typing import Any, List, Mapping, Optional -from .grants import _op, _payload # dict/LogEntry-tolerant accessors +from .grants import _op, _payload, active_grants # dict/LogEntry-tolerant accessors from .model import Op @@ -90,8 +90,16 @@ def pending_requests(entries: List[Mapping[str, Any]], self_did: str) -> List[di def can_open(entries: List[Mapping[str, Any]], req_id: str, self_did: str, now_ts: str) -> tuple[bool, str]: """Whether the box may open a session for req_id: request exists, was - accepted, is authorized for this box (self-authored, or standing mode), - and NO session is currently active (single-session invariant).""" + accepted, is authorized for this box (self-authored, or standing mode + BACKED BY an active capability="assist" Grant), and NO session is + currently active (single-session invariant). + + Per-incident mode: the box-authored ASSIST_REQUEST IS the authority — + no grant needed. Standing mode: the request alone is NOT enough; the + center must additionally hold an active, SELF-issued (issued_by == + self_did — see active_grants()'s sovereignty filter) capability="assist" + grant naming that same center_did, or the open is refused. + """ reqs = {p.get("req_id"): p for p in _by(entries, Op.ASSIST_REQUEST)} if req_id not in reqs: return False, "no-such-request" @@ -101,6 +109,14 @@ def can_open(entries: List[Mapping[str, Any]], req_id: str, req = reqs[req_id] if req.get("issued_by") != self_did and req.get("mode") != "standing": return False, "not-authorized" + if req.get("mode") == "standing": + grants = active_grants(entries, self_did) + has_assist_grant = any( + g.get("capability") == "assist" and g.get("center_did") == req.get("center_did") + for g in grants.values() + ) + if not has_assist_grant: + return False, "no-standing-grant" if active_session(entries, self_did, now_ts) is not None: return False, "session-already-active" return True, "ok" diff --git a/packages/secubox-annuaire/tests/test_assist_resolve.py b/packages/secubox-annuaire/tests/test_assist_resolve.py index 02dcf028..a39d6f3f 100644 --- a/packages/secubox-annuaire/tests/test_assist_resolve.py +++ b/packages/secubox-annuaire/tests/test_assist_resolve.py @@ -104,3 +104,63 @@ def test_can_open_allows_self_authored_accepted_request(): ] ok, reason = assist.can_open(entries, "r-self", BOX, now_ts="2026-07-25T12:00:00Z") assert (ok, reason) == (True, "ok") + + +# --------------------------------------------------------------------------- +# Standing mode REQUIRES an active capability="assist" Grant for the center +# (spec: per-incident's own box-authored request IS the authority; standing +# is NOT — it must be backed by a real, self-issued delegation). +# --------------------------------------------------------------------------- + +def _grant_issue(gid, center_did, capability, scope, issued_by): + return {"op": Op.GRANT_ISSUE.value, "payload": { + "grant_id": gid, "center_did": center_did, "capability": capability, + "scope": scope, "layer": "baseline", "issued_by": issued_by}} + + +def test_can_open_standing_without_grant_is_refused(): + entries = [ + e(Op.ASSIST_REQUEST, req_id="r-standing", center_did=CENTER, + mode="standing", scope="dns", issued_by=CENTER), + e(Op.ASSIST_ACCEPT, req_id="r-standing", issued_by=CENTER), + ] + ok, reason = assist.can_open(entries, "r-standing", BOX, now_ts="2026-07-25T12:00:00Z") + assert (ok, reason) == (False, "no-standing-grant") + + +def test_can_open_standing_with_active_assist_grant_is_allowed(): + entries = [ + e(Op.ASSIST_REQUEST, req_id="r-standing", center_did=CENTER, + mode="standing", scope="dns", issued_by=CENTER), + e(Op.ASSIST_ACCEPT, req_id="r-standing", issued_by=CENTER), + _grant_issue("g1", CENTER, "assist", "dns", BOX), + ] + ok, reason = assist.can_open(entries, "r-standing", BOX, now_ts="2026-07-25T12:00:00Z") + assert (ok, reason) == (True, "ok") + + +def test_can_open_standing_ignores_non_assist_capability_grant(): + # A "config" capability grant for the same center must not satisfy the + # standing assist-grant requirement — capability must be exactly "assist". + entries = [ + e(Op.ASSIST_REQUEST, req_id="r-standing", center_did=CENTER, + mode="standing", scope="dns", issued_by=CENTER), + e(Op.ASSIST_ACCEPT, req_id="r-standing", issued_by=CENTER), + _grant_issue("g1", CENTER, "config", "dns", BOX), + ] + ok, reason = assist.can_open(entries, "r-standing", BOX, now_ts="2026-07-25T12:00:00Z") + assert (ok, reason) == (False, "no-standing-grant") + + +def test_can_open_standing_ignores_foreign_issued_grant(): + # A capability="assist" grant that THIS box never issued (e.g. a mesh + # peer's federated grant naming itself) must not count — active_grants' + # sovereignty filter already scopes this to issued_by == self_did. + entries = [ + e(Op.ASSIST_REQUEST, req_id="r-standing", center_did=CENTER, + mode="standing", scope="dns", issued_by=CENTER), + e(Op.ASSIST_ACCEPT, req_id="r-standing", issued_by=CENTER), + _grant_issue("g1", CENTER, "assist", "dns", OTHER), + ] + ok, reason = assist.can_open(entries, "r-standing", BOX, now_ts="2026-07-25T12:00:00Z") + assert (ok, reason) == (False, "no-standing-grant")