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}] |
|
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}] |
|
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}] |
|
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 |
|
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 |
|
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) |
|
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}
|
|
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}] |
|
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_endpoints — init_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 ImportError — sonador_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 assession.query(cs, w, w.group, w.user).join(w, cs.uid == w.resource)— i.e. one row per (study, worklist-item) pair, withwbound to the specific joined worklist-item row. -
apply_worklist_queryfilter()then applies group scoping viadcm_resources.filter(cs.worklist_reviewer.any(_groupfilter))— butcs.worklist_revieweris a separate relationship traversal (all worklist items for the study), and.any(...)generates a correlatedEXISTSsubquery 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
EXISTScheck 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-joinedw.group(e.g.w.group.in_(...)) rather than via the unrelatedcs.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
-
✅ Archive download revoke leg —tests_download.py, passing live. -
✅ QIDO-style study search visibility — new test, rawrequests, passing live. -
✅ WADO-equivalent retrieval permission boundary — new test, rawrequests, passing live. -
🔴 Worklist browse endpoint —tests_ext_worklists.py, written and run live; caught a real cross-group worklist leak (see above). Test currently fails, correctly. -
Series tag endpoint — not implemented as a test; confirmed dead/broken registration (see table above). Recommend a separate
orthanc-sonadorissue to either wire upinit_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).