Add 'acl' action token for ACL policy-management route ancestor traversal

Summary

Companion MR to oak-tree/medical-imaging/sonador!91 and orthanc-sonador!56 (merged), which together enforce the acl permission for resource ACL policy-management requests (sonador#73 (closed)). All three must deploy together — see Deployment.

Background

DefaultAuthorizationParser::ClassifyAction() maps a request URI to a bounded, closed-enum action token (currently "comment" for .../comments and "worklist" for .../worklists), applied to every level of the resource hierarchy a sub-resource request explodes into. This lets the Sonador consumer evaluate a meaningful permission at each ancestor level instead of falling back to a broad modify check — the same problem the worklist token was added to solve.

The internal ACL policy-management route (/{patients|studies|series}/{id}/acl/{user|group}) is recognized by the plugin's generic resourcesPattern_ (its trailing (|/.*)$ wildcard matches any sub-path), so it explodes into the full patient → study → series hierarchy exactly like modify/remove/comment/worklist requests, with only the leaf carrying a non-empty resource (IncludeResourceUri). Without a dedicated action token, an ancestor of this request is indistinguishable from a generic modify/remove ancestor request at the Sonador consumer, which would require a real modify/remove grant just to satisfy the traversal — privilege unrelated to what managing a resource's access policies actually needs, and in direct conflict with the "acl does not imply view" design decision that also rules out reusing view here.

Changes

Plugin/DefaultAuthorizationParser.cpp / .h:

  • ClassifyAction() recognizes a whole path segment equal to "acl", returning the token "acl".
  • Matches on a whole SEGMENT (identical to the existing "comments"/"worklists" checks), so it does not match the unrelated .../resource-acl permission-lookup route, which is a single token ("resource-acl") that never splits into a bare "acl" segment.
  • Doc comments updated to mention the new token.

Test evidence

Traced against the actual drafted acceptance suite (sonador-ftests!13 (merged), tests/tests_sonadoracl_manage.py, 11 test methods) alongside sonador!91 and orthanc-sonador!56 (merged) — full trace detail is in sonador!91's Test evidence section. This plugin's specific contribution (the "acl" action token) underlies every Global/Local test case exercising the internal route, since it's what lets each exploded ancestor call reach the Sonador consumer's new acl/acl_traverse branch instead of falling through to modify_traverse/remove_traverse. Confirmed the segment-match logic is airtight against the resource-acl collision by construction: Orthanc::Toolbox::SplitString splits the URI on /, and "resource-acl" (hyphen, no /) can never appear as an isolated token equal to "acl". No code changes required as a result of this pass.

Deployment

Deploy with:

  • orthanc-sonador!56 (merged) — emits the acl_traverse policy signal the Sonador consumer checks for ancestors tagged with this token.
  • oak-tree/medical-imaging/sonador!91 — the consumer (ResourceAuthorization.resource_perm()'s new acl branch) that reads this token.

Without the other two, this token is classified but never consulted (no functional effect on its own — this MR alone is a no-op from the API consumer's perspective, though harmless to deploy independently since it purely adds a new closed-enum classification value).

Note: this token only affects the internal ACL-management route. The DICOMweb mirror (/dicom-web/{studies|series}/{uid}/acl/{user|group}) is not recognized by any of this plugin's DICOMweb classification patterns (dicomWebStudies_/dicomWebSeries_ accept only a closed suffix enum — series/metadata/instances/rendered/thumbnail — that does not include acl), so it falls to the "unknown resource → system access" path regardless of this change and is handled entirely on the Sonador side (sonador!91's clean_auth_request() backfill + URI-regex detection).

Edited by Sonador Claude

Merge request reports

Loading