Emit 'acl_traverse' ancestor-traversal signal for local ACL policy builder
Summary
Companion MR to oak-tree/medical-imaging/sonador!91 (enforces the acl permission for resource ACL policy-management requests, per sonador#73 (closed)). Both MRs must deploy together — see Deployment.
Background
The auth plugin's route parser now tags the internal ACL policy-management route's exploded patient → study → series hierarchy with action="acl" (mirroring action="comment"/"worklist" — see companion oak-tree/medical-imaging/orthanc-authorization MR). The ancestor (empty-resource) levels of that hierarchy therefore resolve via ResourceAuthorization.resource_perm()'s new acl branch (sonador!91), which consults an acl_traverse signal instead of the ancestor's own acl permission.
This is necessary because acl propagates downward only — a series-only grant contributes nothing to its parent study (confirmed correct and unchanged in this MR; only the traversal signal is new). Without acl_traverse, a series-scoped acl grant would be denied at the parent study/patient ancestor level, rejecting the whole hierarchy-exploded request even though the leaf (series) itself is legitimately authorized.
This exactly mirrors the existing modify_traverse/remove_traverse pattern established in !53 (merged) / sonador!88.
Changes
sonador_orthanc/auth/web.py:
- New
ACL_PERM_ACL_TRAVERSE = 'acl_traverse'constant, alongsideACL_PERM_MODIFY_TRAVERSE/ACL_PERM_REMOVE_TRAVERSE. -
_fetch_patient_policy(patient ancestor): emitsacl_traverse=Truewhen the patient's ownacl, or a descendant study/seriesaclgrant, is set. -
_fetch_resource_policystudy branch (study ancestor): emitsacl_traverse=Truewhen the study's own (merged)acl, or a descendant seriesaclgrant, is set. -
_fetch_resource_policyseries branch (leaf): emitsacl_traverse=Truemirroring the resolvedaclgrant (irrelevant on the leaf itself, which enforces the realacldirectly, but kept for consistency with modify/remove). - Both response-serialization points (
pick()in_fetch_resource_policy, and the user/group merge inresource_policy) now includeACL_PERM_ACL_TRAVERSEalongside the existing traverse keys, so the signal survives to the Sonador consumer.
Emitted only when True (same rule as modify/remove traverse): an explicit False would survive pick() and make an otherwise-empty no-grant policy non-empty, which the Sonador consumer treats as authoritative local policy instead of deferring to the global policy.
Test evidence
Initial static trace against the drafted acceptance suite (sonador-ftests!13 (merged), tests/tests_sonadoracl_manage.py) claimed no code changes were required. A subsequent live run against the deployed stack found this was wrong. 9/11 tests passed but testacl_local_scoped_study and testacl_local_scoped_series both failed with 403, while the equivalent global-scoped tests and the patient-level local test passed.
Root cause: in _fetch_patient_policy, the acl_traverse block was indented one level too deep — nested inside the body of the remove_traverse if statement instead of being a sibling condition. It only ran when remove_traverse's own condition (requiring a modify/remove grant somewhere in the hierarchy) was also true, which it never is for an acl-only local grant. This left the patient ancestor's acl_traverse unset for exactly the local study/series-scoped acl grants the two failing tests exercise — patient-level local grants need no ancestor traversal (pass trivially), and global-scoped tests don't depend on this builder-computed signal at all, which is why only those two cases were affected. _fetch_resource_policy's study and series branches were already correctly indented as siblings and did not have this bug.
Fixed with a whitespace-only dedent (commit 917bb511). Verified via AST parse and a minimal diff (8 insertions/8 deletions, no logic change). Awaiting a live re-run of testacl_local_scoped_study/testacl_local_scoped_series to confirm.
Deployment
Deploy with:
-
oak-tree/medical-imaging/sonador!91 — the consumer of this signal (
ResourceAuthorization.acl_traverse). Without it, this MR alone has no effect (the key is emitted but never read). -
orthanc-authorization!2 (merged) — without it, the plugin never tags the internal route's ancestor calls with
action="acl", so they never reach sonador!91'saclbranch to consult this signal at all (they'd fall through tomodify_traverse/remove_traverseinstead, denying acl-only ancestors that lack modify/remove).
Reload Orthanc to pick up sonador_orthanc. Verify a system/acl/resource response for a resource with a descendant acl grant now includes acl_traverse: true.
Related issues
- sonador#73 (closed) — full spec this is a precursor to.
- sonador-ftests!13 (merged) — the drafted acceptance suite this MR was verified against.
- Precedent: !53 (merged) / oak-tree/medical-imaging/sonador!88.
- Companion MR: oak-tree/medical-imaging/sonador!91