fix(annuaire): standing-mode assist request now requires an active assist grant

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 <devel@cybermind.fr>
This commit is contained in:
CyberMind-FR 2026-07-25 14:40:35 +02:00
parent d08820d675
commit 6dfc5a5ff0
2 changed files with 79 additions and 3 deletions

View File

@ -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"

View File

@ -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")