changeset 829:ba3295716819 attach-custom-data

integration mainline->attach-custom-data
author Sebastien Jodogne <s.jodogne@gmail.com>
date Thu, 12 Jun 2025 16:59:24 +0200
parents f094845ac9a2 (current diff) b4ac775869b2 (diff)
children 78c81c902938 563b6de9f08d
files NewTests/PostgresUpgrades/downgrade.sh Tests/Tests.py
diffstat 5 files changed, 96 insertions(+), 11 deletions(-) [+]
line wrap: on
line diff
--- a/NewTests/Authorization/auth_service.py	Tue May 27 17:07:49 2025 +0200
+++ b/NewTests/Authorization/auth_service.py	Thu Jun 12 16:59:24 2025 +0200
@@ -78,3 +78,21 @@
 
     logging.info("validate token: " + response.json())
     return response
+
+@app.post("/tokens/decode")
+def decode_token(request: TokenDecoderRequest):
+
+    logging.info("decoding token: " + request.json())
+    response = TokenDecoderResponse(resources=[])
+
+    if request.token_value == "token-a-study" or request.token_value == "token-both-studies":
+        response.resources.append(OrthancResource(level=Levels.STUDY,
+                                                    orthanc_id="b9c08539-26f93bde-c81ab0d7-bffaf2cb-a4d0bdd0",
+                                                    dicom_uid="1.2.840.113619.2.176.2025.1499492.7391.1171285944.390"))
+    if request.token_value == "token-b-study" or request.token_value == "token-both-studies":
+        response.resources.append(OrthancResource(level=Levels.STUDY,
+                                                    orthanc_id="27f7126f-4f66fb14-03f4081b-f9341db2-53925988",
+                                                    dicom_uid="2.16.840.1.113669.632.20.1211.10000357775"))
+
+    logging.info("decoded token: " + response.json())
+    return response
--- a/NewTests/Authorization/models.py	Tue May 27 17:07:49 2025 +0200
+++ b/NewTests/Authorization/models.py	Thu Jun 12 16:59:24 2025 +0200
@@ -1,4 +1,4 @@
-from typing import Optional, List
+from typing import Optional, List, Dict
 from pydantic import BaseModel, Field
 from enum import Enum
 from datetime import datetime
@@ -31,6 +31,7 @@
     MEDDREAM_VIEWER_PUBLICATION = 'meddream-viewer-publication'  # a link to open the MedDream viewer valid for a long period
     STONE_VIEWER_PUBLICATION = 'stone-viewer-publication'  # a link to open the Stone viewer valid for a long period
     OHIF_VIEWER_PUBLICATION = 'ohif-viewer-publication'  # a link to open the OHIF viewer valid for a long period
+    VOLVIEW_VIEWER_PUBLICATION = 'volview-viewer-publication'  # a link to open the VolView viewer valid for a long period
 
     MEDDREAM_INSTANT_LINK = 'meddream-instant-link'  # a direct link to MedDream viewer that is valid only a few minutes to open the viewer directly
 
@@ -80,7 +81,6 @@
     level: Optional[Levels]
     method: Methods
     uri: Optional[str] = None
-#     labels: Optional[List[str]]
 
 
 class TokenValidationResponse(BaseModel):
@@ -97,7 +97,7 @@
     token_type: Optional[TokenType] = Field(alias="token-type", default=None)
     error_code: Optional[DecoderErrorCodes] = Field(alias="error-code", default=None)
     redirect_url: Optional[str] = Field(alias="redirect-url", default=None)
-
+    resources: List[OrthancResource]
 
 class UserProfileRequest(BaseModel):
     token_key: Optional[str] = Field(alias="token-key", default=None)
@@ -118,16 +118,30 @@
     SETTINGS = 'settings'
     API_VIEW = 'api-view'
     EDIT_LABELS = 'edit-labels'
+    ADMIN_PERMISSIONS = 'admin-permissions'
 
     SHARE = 'share'
 
 
-class UserProfileResponse(BaseModel):
-    name: str
+class RolePermissions(BaseModel):
     authorized_labels: List[str] = Field(alias="authorized-labels", default_factory=list)
     permissions: List[UserPermissions] = Field(default_factory=list)
+
+    class Config:
+        use_enum_values = True
+        populate_by_name = True  # allow creating object from dict (used when deserializing the permission file)
+
+
+class UserProfileResponse(RolePermissions):
+    name: str
+    # authorized_labels: List[str] = Field(alias="authorized-labels", default_factory=list)
+    # permissions: List[UserPermissions] = Field(default_factory=list)
     validity: int
 
     class Config:
         use_enum_values = True
-        populate_by_name = True
\ No newline at end of file
+        populate_by_name = True
+
+class RolesConfigurationModel(BaseModel):
+    roles: Dict[str, RolePermissions]                                                # role/permissions mapping
+    available_labels: List[str] = Field(alias="available-labels", default_factory=list)  # if empty, everyone can create additionnal labels, if not, they can only add/remove the listed labels
--- a/NewTests/Authorization/test_authorization.py	Tue May 27 17:07:49 2025 +0200
+++ b/NewTests/Authorization/test_authorization.py	Thu Jun 12 16:59:24 2025 +0200
@@ -371,9 +371,11 @@
         # with a resource token, we can access only the given resource, not generic resources or resources from other studies
 
         # generic resources are forbidden
+        # note: even tools/find is still forbidden in 0.9.3 (but not /dicom-web/studies -> see below)
         self.assert_is_forbidden(lambda: o.studies.find(query={"PatientName": "KNIX"},  # tools/find is forbidden with a resource token
                                                         labels=['label_b'],
                                                         labels_constraint='Any'))
+
         self.assert_is_forbidden(lambda: o.get_all_labels())
         self.assert_is_forbidden(lambda: o.studies.get_all_ids())
         self.assert_is_forbidden(lambda: o.patients.get_all_ids())
@@ -415,6 +417,12 @@
         o.get_json(f"dicom-web/series?0020000D={self.label_a_study_dicom_id}")
         o.get_json(f"dicom-web/instances?0020000D={self.label_a_study_dicom_id}")
 
+        if o.is_plugin_version_at_least("authorization", 0, 9, 3):
+            # equivalent to the prior studies request in OHIF
+            self.assertEqual(1, len(o.get_json(f"dicom-web/studies?PatientID={self.label_a_patient_dicom_id}")))
+            self.assertEqual(0, len(o.get_json(f"dicom-web/studies?PatientID={self.label_b_patient_dicom_id}")))
+
+
         if self.o.is_orthanc_version_at_least(1, 12, 2):
             o.get_binary(f"tools/create-archive?resources={self.label_a_study_id}")
             o.get_binary(f"tools/create-archive?resources={self.label_a_series_id}")
--- a/NewTests/PostgresUpgrades/downgrade.sh	Tue May 27 17:07:49 2025 +0200
+++ b/NewTests/PostgresUpgrades/downgrade.sh	Thu Jun 12 16:59:24 2025 +0200
@@ -4,12 +4,15 @@
 
 apt-get update && apt-get install -y wget mercurial
 hg clone https://orthanc.uclouvain.be/hg/orthanc-databases
+pushd orthanc-databases
+
 # TODO: change attach-custom-data by the plugin version number or "default" !
-pushd orthanc-databases
 hg update -r attach-custom-data
 
 psql -U postgres -f /scripts/orthanc-databases/PostgreSQL/Plugins/SQL/Downgrades/Rev5ToRev4.sql
+psql -U postgres -f /scripts/orthanc-databases/PostgreSQL/Plugins/SQL/Downgrades/Rev4ToRev3.sql
 
 # if you want to test a downgrade procedure, you may use this code ...
 # psql -U postgres -f downgrade.sql
 popd
+popd
--- a/Tests/Tests.py	Tue May 27 17:07:49 2025 +0200
+++ b/Tests/Tests.py	Thu Jun 12 16:59:24 2025 +0200
@@ -907,9 +907,19 @@
         self.assertEqual(0, DoGet(_REMOTE, '/patients/%s/protected' % a))
         DoPut(_REMOTE, '/patients/%s/protected' % a, '1', 'text/plain')
         self.assertEqual(1, DoGet(_REMOTE, '/patients/%s/protected' % a))
+
+        if IsOrthancVersionAbove(_REMOTE, 1, 12, 8):
+            p = DoGet(_REMOTE, '/patients/%s' % a)
+            self.assertIn('IsProtected', p)
+            self.assertTrue(p['IsProtected'])
+
         DoPut(_REMOTE, '/patients/%s/protected' % a, '0', 'text/plain')
         self.assertEqual(0, DoGet(_REMOTE, '/patients/%s/protected' % a))
 
+        if IsOrthancVersionAbove(_REMOTE, 1, 12, 8):
+            p = DoGet(_REMOTE, '/patients/%s' % a)
+            self.assertIn('IsProtected', p)
+            self.assertFalse(p['IsProtected'])
 
     def test_raw_tags(self):
         i = UploadInstance(_REMOTE, 'PrivateTags.dcm')['ID']
@@ -10872,13 +10882,21 @@
 
         a = DoGet(_REMOTE, '/patients?expand')
         self.assertEqual(1, len(a))
-        self.assertEqual(7, len(a[0]))
+        if IsOrthancVersionAbove(_REMOTE, 1, 12, 8):
+            self.assertEqual(8, len(a[0]))
+            self.assertTrue('IsProtected' in a[0])
+        else:
+            self.assertEqual(7, len(a[0]))
         CheckPatientContent(a[0])
         self.assertFalse('RequestedTags' in a[0])
 
         a = DoGet(_REMOTE, '/patients?expand&requestedTags=%s' % requestedTags)
         self.assertEqual(1, len(a))
-        self.assertEqual(8, len(a[0]))
+        if IsOrthancVersionAbove(_REMOTE, 1, 12, 8):
+            self.assertEqual(9, len(a[0]))
+            self.assertTrue('IsProtected' in a[0])
+        else:
+            self.assertEqual(8, len(a[0]))
         CheckPatientContent(a[0])
         CheckRequestedTags(a[0])
 
@@ -10919,12 +10937,20 @@
         CheckRequestedTags(a[0])
 
         a = DoGet(_REMOTE, '/patients/%s' % u['ParentPatient'])
-        self.assertEqual(7, len(a))
+        if IsOrthancVersionAbove(_REMOTE, 1, 12, 8):
+            self.assertEqual(8, len(a))
+            self.assertTrue('IsProtected' in a)
+        else:
+            self.assertEqual(7, len(a))
         CheckPatientContent(a)
         self.assertFalse('RequestedTags' in a)
 
         a = DoGet(_REMOTE, '/patients/%s?requestedTags=%s' % (u['ParentPatient'], requestedTags))
-        self.assertEqual(8, len(a))
+        if IsOrthancVersionAbove(_REMOTE, 1, 12, 8):
+            self.assertEqual(9, len(a))
+            self.assertTrue('IsProtected' in a)
+        else:
+            self.assertEqual(8, len(a))
         CheckPatientContent(a)
         CheckRequestedTags(a)
 
@@ -11519,6 +11545,7 @@
             self.assertIn('IsStable', a[0])
             self.assertNotIn('Attachments', a[0])
             self.assertNotIn('Metadata', a[0])
+            self.assertNotIn('IsProtected', a[0])
 
 
             a = DoPost(_REMOTE, '/tools/find', {    'Level' : 'Series',
@@ -11542,6 +11569,7 @@
             self.assertNotIn('IsStable', a[0])
             self.assertNotIn('Attachments', a[0])
             self.assertNotIn('Metadata', a[0])
+            self.assertNotIn('IsProtected', a[0])
 
 
             a = DoPost(_REMOTE, '/tools/find', {    'Level' : 'Series',
@@ -11564,6 +11592,7 @@
             self.assertIn('Status', a[0])
             self.assertIn('IsStable', a[0])
             self.assertNotIn('Attachments', a[0])
+            self.assertNotIn('IsProtected', a[0])
 
 
             a = DoPost(_REMOTE, '/tools/find', {    'Level' : 'Instances',
@@ -11585,6 +11614,7 @@
             self.assertIn('Labels', a[0])
             self.assertNotIn('Attachments', a[0])
             self.assertNotIn('Metadata', a[0])
+            self.assertNotIn('IsProtected', a[0])
 
             a = DoPost(_REMOTE, '/tools/find', {    'Level' : 'Instances',
                                                     'Query' : { 
@@ -11616,6 +11646,18 @@
             self.assertIn('RequestedTags', a[0]) # the RequestedTags are always in the response as soon as you have requested them
             self.assertIn('SOPClassUID', a[0]['RequestedTags'])
 
+        if IsOrthancVersionAbove(_REMOTE, 1, 12, 8):
+            a = DoPost(_REMOTE, '/tools/find', {    'Level' : 'Patients',
+                                                    'Query' : { 
+                                                    },
+                                                    'ResponseContent' : ['IsProtected']
+                                                    })
+
+            self.assertIn('ID', a[0])            # the ID is always in the response
+            self.assertIn('Type', a[0])          # the Type is always in the response
+            self.assertIn('IsProtected', a[0])
+
+
 
     def test_extended_find_full(self):
         if IsOrthancVersionAbove(_REMOTE, 1, 12, 5) and HasExtendedFind(_REMOTE):