DICOMweb Auth Endpoints: add test cases to ensure that the DICOMweb endpoints used by the sharing system are equivalent to the Orthanc endpoints

Original scope

  • Test Case 1: Study permissions
    • Setup test with admin user to manage upload and config, test user (test access), and a test group to which the user will be assigned
    • Upload a sample study
    • Set permissions on the study for the test user to be able to access (view) using the DICOMweb endpoint
    • Ensure policy appears using the Orthanc internal API
    • Use test user to download series
    • Revoke permission policy
    • Ensure that the user receives a 403 error when attempting to access the data
    • Ensure that the study no longer appears in search results
  • Test Case 2: Series permissions
    • Setup test (refer to description above)
    • Upload a sample series
    • Set permissions on the series for the test user (view) using DICOMweb endpoint
    • Ensure policy appears via Orthanc internal API
    • Use test user to download series
    • Revoke permission policy
    • Ensure that the user receives a 403 error

Refined scope (2026-07)

The checklist above is narrow (ACL grant/revoke via /acl/user + one download), and doesn't say which DICOMweb endpoints it's actually protecting. A full pass over orthanc-sonador's sonador_orthanc/web/dicomweb.py (the module that registers every /dicom-web/... route) plus the current sonador-ftests/sonador-client suite shows the real picture is broader. Below is the complete endpoint inventory and what's actually covered today.

Endpoint coverage inventory

Endpoint family Route(s) Coverage Notes
ACL policy management {studies|series}/{uid}/acl/{user|group}[/{policy-uid}] Covered tests/tests_sonadoracl_manage.py (sonador#73 (closed)/!13 (merged)) — grant/revoke/negative, 11/11 passing. Positive DICOMweb grant is a documented residual gap there.
Comments (CRUD) {studies|series}/{uid}/comments[/{comment-uid}] Covered, thorough sonador/ftests/tests_ext_comments.py — DICOMweb vs standard API parity, global + local ACL enforcement, all 4 HTTP methods, positive + negative. Best-covered endpoint family in the suite.
Worklist management studies/{uid}/worklists[/{worklist-uid}] Covered sonador/ftests/tests_ext_worklists.py, tests/tests_ext_worklist_procedures.py — DICOMweb create/get/update, permission boundaries, review-history arc.
Worklist browse worklist/studies 🔴 Test written, live server bug found ImagingServer.fetch_user_dcmweb_worklist() existed but was unused by any test. Added test_worklist_browse_dicomweb_reassignment to tests_ext_worklists.py and it caught a real cross-group data leak — see dedicated section below. Test currently fails against dev01 (correctly — it asserts the secure behavior).
Archive download {studies|series}/{uid}/archive Covered (added 2026-07) sonador/ftests/tests_download.py already covered admin + ACL-granted download for both study and series. Added test_dcmweb_download_study_acl_revoked / test_dcmweb_download_series_acl_revoked: grant → succeeds → revoke → 403. Both pass live. Closes the revoke leg of the original Test Case 1/2 above.
Study search (QIDO-like) studies (bare list) Covered (added 2026-07) CacheStudyDicomWebListView — Sonador's own ACL-filtered study search (SecureResourceQueryViewMixin), independent of the Orthanc-plugin gate. No client wrapper exists; new test uses raw requests, passes live. Closes the "study no longer appears in search results" leg of the original scope. Note: a server ACL with query: True bypasses this filtering entirely by design (matches the existing tests_sonadoracl_query.py convention) — the test deliberately omits query to exercise the ACL-filtered path.
Series/study metadata, instance retrieval, frame retrieval studies/{uid}/series[/{uid}/metadata], .../instances/{uid}, .../instances/{uid}/frames/{n} Covered (added 2026-07) The actual WADO-RS-equivalent paths the viewer's image loader depends on for pixel data. clean_auth_request() back-fills level/UID for these (ORTHANC_DICOMWEB_INTERNAL_*_REGEX), reducing enforcement to a plain view check. No client wrapper exists; new tests use raw requests, all 3 pass live against dev01. Most viewer-critical addition in this pass.
Series tag series/{uid}/tag[/{tag-uid}] Dead code, confirmed — not a test target SeriesTagItemDICOMManagementView/...RestView, imported and registered by dicomweb.py::init_tag_endpoints(). Two independent problems, both confirmed by reading source directly: (1) init_tag_endpoints() is never called anywhere in the plugin bootstrap (sonador-plugin.py's orthanc_cache_onstart calls init_cached_endpoints/init_ext_endpoints/init_auth_endpoints/init_distortionfilter_endpoints/init_worklist_endpints/init_download_endpointsinit_tag_endpoints is not in that list), so the route is never registered and a request to it 404s, full stop. (2) Even if it were called, it would raise ImportErrorsonador_orthanc/web/tag.py only defines TagItemManagementView/TagItemRestView (the group-scoped tag API, registered separately and directly in sonador-plugin.py at /groups/{gid}/tags[/{tag-uid}] — this is what tests_ext_tags.py/tests_sonadoracl_roles.py already test), not the SeriesTagItemDICOM* names dicomweb.py tries to import. This is not an ACL enforcement gap to write a test against — there is no running endpoint to hit. Dead/broken code someone should either wire up correctly or delete; flagged for the server-side team rather than pursued as a test case here.
Distortion filter groups/{gid}/distortion-filter/... Out of scope Specialized/research capability, not part of the standard viewing workflow the viewer depends on. Not pursued here.

🔴 Confirmed finding: worklist browse endpoint leaks worklist items across groups

test_worklist_browse_dicomweb_reassignment (new, tests_ext_worklists.py) sets up two isolated group/user pairs, each with only server-level worklist: True and a local View ACL on the same test study, and assigns a worklist item to group A only. Running it against dev01: group B's user sees group A's worklist item in their own /dicom-web/worklist/studies browse list, confirmed via direct inspection (each returned item's group field was printed and checked) — not a test-side timing or setup mistake (ACL propagation delays up to 1.5s were ruled out; direct DB-level group membership for both test users was independently verified clean).

Root cause, traced in orthanc-sonador's sonador_orthanc/worklist/web.py (StudyReviewerWorklistItemDICOMListView):

  • get_base_resourcelist() builds the base query as session.query(cs, w, w.group, w.user).join(w, cs.uid == w.resource) — i.e. one row per (study, worklist-item) pair, with w bound to the specific joined worklist-item row.
  • apply_worklist_queryfilter() then applies group scoping via dcm_resources.filter(cs.worklist_reviewer.any(_groupfilter)) — but cs.worklist_reviewer is a separate relationship traversal (all worklist items for the study), and .any(...) generates a correlated EXISTS subquery answering "does this study have some worklist item matching my group?", not "does this joined row match my group?".
  • Net effect: once a study has even one worklist item belonging to a group the requesting user is a member of, the EXISTS check passes for the whole study, and every row from the outer join — including worklist items belonging to other groups entirely — is returned unfiltered. The fix should filter directly on the already-joined w.group (e.g. w.group.in_(...)) rather than via the unrelated cs.worklist_reviewer.any(...) relationship.
  • This is a real cross-tenant information disclosure: any user with worklist access to a study that has ever had items assigned to multiple groups sees every group's assignments for that study through the browse endpoint, regardless of their own membership.

A server-side hand-off covering this and the dead series-tag registration below has been prepared for the orthanc-sonador team; not something to work around in the test. The test itself is written correctly (asserts the secure/expected behavior) and is left failing against dev01 to document the gap — do not weaken the assertion to make it pass.

Test plan status

  1. Archive download revoke legtests_download.py, passing live.
  2. QIDO-style study search visibility — new test, raw requests, passing live.
  3. WADO-equivalent retrieval permission boundary — new test, raw requests, passing live.
  4. 🔴 Worklist browse endpointtests_ext_worklists.py, written and run live; caught a real cross-group worklist leak (see above). Test currently fails, correctly.
  5. Series tag endpoint — not implemented as a test; confirmed dead/broken registration (see table above). Recommend a separate orthanc-sonador issue to either wire up init_tag_endpoints() correctly or remove the dead code.

Status

All client-side test changes above are implemented and pushed: oak-tree/medical-imaging/sonador-client!75 (branch claude/dcmweb-viewer-tests). Two real server-side defects found in this pass (worklist browse cross-group leak; dead/broken series-tag endpoint registration) are documented above and handed off separately for orthanc-sonador. This issue should stay open until sonador-client!75 merges and the server-side fixes land (at which point test_worklist_browse_dicomweb_reassignment should be re-run to confirm it goes green).

Edited by Sonador Claude