changeset 338:efc9b2a319bd

Support new /tools/bulk-delete payload
author Alain Mazy <am@orthanc.team>
date Fri, 17 Jul 2026 14:54:34 +0200
parents da8f5bd16407
children 0a237fbff7bb
files NEWS Plugin/AuthorizationParserBase.cpp Plugin/AuthorizationParserBase.h Plugin/DefaultAuthorizationParser.cpp Plugin/Plugin.cpp
diffstat 5 files changed, 117 insertions(+), 82 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Thu Apr 23 10:24:18 2026 +0200
+++ b/NEWS	Fri Jul 17 14:54:34 2026 +0200
@@ -1,3 +1,8 @@
+Pending changes in the mainline
+===============================
+
+* Support new /tools/bulk-delete payload
+
 2026-04-23 - v 0.11.3
 =====================
 
--- a/Plugin/AuthorizationParserBase.cpp	Thu Apr 23 10:24:18 2026 +0200
+++ b/Plugin/AuthorizationParserBase.cpp	Fri Jul 17 14:54:34 2026 +0200
@@ -38,35 +38,34 @@
   }
     
 
-  Orthanc::ResourceType AuthorizationParserBase::AddOrthancUnknownResource(AccessedResources& target,
-                                                                           const std::string& orthancId)
+  bool AuthorizationParserBase::LookupOrthancUnknownResourceLevel(Orthanc::ResourceType& level, const std::string& orthancId)
   {
     std::string dicomId;
     if (resourceHierarchy_->LookupDicomUid(dicomId, Orthanc::ResourceType_Study, orthancId))
     {
-      AddOrthancStudy(target, orthancId);
-      return Orthanc::ResourceType_Study;
+      level = Orthanc::ResourceType_Study;
+      return true;
     }
 
     if (resourceHierarchy_->LookupDicomUid(dicomId, Orthanc::ResourceType_Patient, orthancId))
     {
-      AddOrthancPatient(target, orthancId);
-      return Orthanc::ResourceType_Patient;
+      level = Orthanc::ResourceType_Patient;
+      return true;
     }
 
     if (resourceHierarchy_->LookupDicomUid(dicomId, Orthanc::ResourceType_Series, orthancId))
     {
-      AddOrthancSeries(target, orthancId);
-      return Orthanc::ResourceType_Series;
+      level = Orthanc::ResourceType_Series;
+      return true;
     }
 
     if (resourceHierarchy_->LookupDicomUid(dicomId, Orthanc::ResourceType_Instance, orthancId))
     {
-      AddOrthancInstance(target, orthancId);
-      return Orthanc::ResourceType_Instance;
+      level = Orthanc::ResourceType_Instance;
+      return true;
     }
 
-    throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
+    return false;
   }
 
   void AuthorizationParserBase::AddOrthancResource(AccessedResources& target,
--- a/Plugin/AuthorizationParserBase.h	Thu Apr 23 10:24:18 2026 +0200
+++ b/Plugin/AuthorizationParserBase.h	Fri Jul 17 14:54:34 2026 +0200
@@ -68,8 +68,7 @@
                           const std::string& instanceDicomUid);
 
   public:
-    Orthanc::ResourceType AddOrthancUnknownResource(AccessedResources& target,
-                                                    const std::string& orthancId);
+    bool LookupOrthancUnknownResourceLevel(Orthanc::ResourceType& level, const std::string& orthancId);
 
     void AddOrthancResource(AccessedResources& target,
                             Orthanc::ResourceType type,
--- a/Plugin/DefaultAuthorizationParser.cpp	Thu Apr 23 10:24:18 2026 +0200
+++ b/Plugin/DefaultAuthorizationParser.cpp	Fri Jul 17 14:54:34 2026 +0200
@@ -170,7 +170,11 @@
 
       for (std::set<std::string>::const_iterator it = resourcesIds.begin(); it != resourcesIds.end(); ++it)
       {
-        AddOrthancUnknownResource(target, *it);
+        Orthanc::ResourceType level;
+        if (LookupOrthancUnknownResourceLevel(level, *it))
+        {
+          AddOrthancResource(target, level, *it);
+        }
       }
       
       return true;
--- a/Plugin/Plugin.cpp	Thu Apr 23 10:24:18 2026 +0200
+++ b/Plugin/Plugin.cpp	Fri Jul 17 14:54:34 2026 +0200
@@ -94,6 +94,27 @@
   }
 }
 
+OrthancPluginResourceType Convert(Orthanc::ResourceType type)
+{
+  switch (type)
+  {
+    case Orthanc::ResourceType_Patient:
+      return OrthancPluginResourceType_Patient;
+
+    case Orthanc::ResourceType_Study:
+      return OrthancPluginResourceType_Study;
+
+    case Orthanc::ResourceType_Series:
+      return OrthancPluginResourceType_Series;
+
+    case Orthanc::ResourceType_Instance:
+      return OrthancPluginResourceType_Instance;
+
+    default:
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+  }
+}
+
 
 static const char* KEY_USER_DATA = "UserData";
 static const char* KEY_USER_ID = "AuditLogsUserId";
@@ -1021,8 +1042,66 @@
   }
 }
 
-
-bool CheckHasAccessToAllResourcesInPayload(OrthancPluginRestOutput* output, const OrthancPluginHttpRequest* request)
+static const char* const KEY_RESOURCES = "Resources";
+static const char* const KEY_LEVEL = "Level";
+static const char* const KEY_ID = "ID";
+
+void GetResourcesList(std::map<std::string, Orthanc::ResourceType>& resources, Json::Value parsedPayload, const OrthancPluginHttpRequest* request)
+{
+  if (!OrthancPlugins::ReadJson(parsedPayload, request->body, request->bodySize) || !parsedPayload.isMember(KEY_RESOURCES) || !parsedPayload[KEY_RESOURCES].isArray())
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "A JSON payload with a 'Resources' string array field was expected");
+  }
+
+  Orthanc::ResourceType levelInRootPayload = Orthanc::ResourceType_Instance; // random value
+  bool hasLevelAtRootInPayload = parsedPayload.isMember(KEY_LEVEL) && parsedPayload[KEY_LEVEL].isString();
+  
+  if (hasLevelAtRootInPayload)
+  {
+    levelInRootPayload = Orthanc::StringToResourceType(parsedPayload[KEY_LEVEL].asString().c_str());
+  }
+  
+  for (Json::ArrayIndex i = 0; i < parsedPayload[KEY_RESOURCES].size(); ++i)
+  {
+    OrthancPlugins::IAuthorizationParser::AccessedResources accessedResources;
+
+    const Json::Value& resource = parsedPayload[KEY_RESOURCES][i];
+    std::string resourceId;
+
+    if (resource.isString())
+    {
+      resourceId = parsedPayload[KEY_RESOURCES][i].asString();
+
+      if (hasLevelAtRootInPayload)
+      {
+        resources[resourceId] = levelInRootPayload;
+      }
+      else
+      {
+        Orthanc::ResourceType level;
+        if (authorizationParser_->LookupOrthancUnknownResourceLevel(level, resourceId))
+        {
+          resources[resourceId] = level;
+        }
+        else
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource, std::string("resource ID: ") + resourceId);
+        }
+      }
+    }
+    else if (resource.isObject() && resource.isMember(KEY_ID) && resource[KEY_ID].isString()
+          && resource.isMember(KEY_LEVEL) && resource[KEY_LEVEL].isString())
+    {
+      Orthanc::ResourceType level = Orthanc::StringToResourceType(resource[KEY_LEVEL].asString().c_str());
+      
+      resourceId = resource[KEY_ID].asString();
+      resources[resourceId] = level;
+    }
+  }
+}
+
+
+bool CheckHasAccessToAllResourcesInPayload(const std::map<std::string, Orthanc::ResourceType>& resources, OrthancPluginRestOutput* output, const OrthancPluginHttpRequest* request)
 {
   // make sur the user has access to all resources listed in "Resources" (with potentialy a "Level" field to help identify the resources)
 
@@ -1037,34 +1116,11 @@
     return true;
   }
 
-  Json::Value payload;
-  if (!OrthancPlugins::ReadJson(payload, request->body, request->bodySize) || !payload.isMember("Resources") || !payload["Resources"].isArray())
-  {
-    throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "A JSON payload with a 'Resources' string array field was expected");
-  }
-
-  Orthanc::ResourceType levelInPayload = Orthanc::ResourceType_Instance; // random value
-  bool hasLevelInPayload = payload.isMember("Level") && payload["Level"].isString();
-  
-  if (hasLevelInPayload)
+  for (std::map<std::string, Orthanc::ResourceType>::const_iterator it = resources.begin(); it != resources.end(); ++it)
   {
-    levelInPayload = Orthanc::StringToResourceType(payload["Level"].asString().c_str());
-  }
-  
-  for (Json::ArrayIndex i = 0; i < payload["Resources"].size(); ++i)
-  {
-    std::string resourceId = payload["Resources"][i].asString();
-
     OrthancPlugins::IAuthorizationParser::AccessedResources accessedResources;
 
-    if (hasLevelInPayload)
-    {
-      authorizationParser_->AddOrthancResource(accessedResources, levelInPayload, resourceId);
-    }
-    else
-    {
-      authorizationParser_->AddOrthancUnknownResource(accessedResources, resourceId);
-    }
+    authorizationParser_->AddOrthancResource(accessedResources, it->second, it->first);
 
     bool granted = false;
 
@@ -1075,7 +1131,7 @@
 
     if (!granted)
     {
-      LOG(WARNING) << "Auth plugin: the user does not have access to resource " << resourceId;
+      LOG(WARNING) << "Auth plugin: the user does not have access to resource " << it->first;
       OrthancPlugins::AnswerHttpError(Orthanc::HttpStatus_403_Forbidden, output);
       return false;
     }
@@ -1428,12 +1484,11 @@
   }
 
   Json::Value payload;
-  if (!OrthancPlugins::ReadJson(payload, request->body, request->bodySize) || !payload.isMember("Resources"))
-  {
-    throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "A JSON payload was expected");
-  }
-
-  if (!CheckHasAccessToAllResourcesInPayload(output, request))
+  std::map<std::string, Orthanc::ResourceType> resources;
+  
+  GetResourcesList(resources, payload, request);
+
+  if (!CheckHasAccessToAllResourcesInPayload(resources, output, request))
   {
     return;
   }
@@ -1682,30 +1737,6 @@
 }
 
 
-OrthancPluginResourceType IdentifyResourceType(const std::string& resourceId)
-{
-  Json::Value v;
-
-  if (OrthancPlugins::RestApiGet(v, "/studies/" + resourceId, false))
-  {
-    return OrthancPluginResourceType_Study;
-  }
-  if (OrthancPlugins::RestApiGet(v, "/patients/" + resourceId, false))
-  {
-    return OrthancPluginResourceType_Patient;
-  }
-  if (OrthancPlugins::RestApiGet(v, "/series/" + resourceId, false))
-  {
-    return OrthancPluginResourceType_Series;
-  }
-  if (OrthancPlugins::RestApiGet(v, "/instances/" + resourceId, false))
-  {
-    return OrthancPluginResourceType_Instance;
-  }
-
-  return OrthancPluginResourceType_None;
-}
-
 
 void GetResourceDeletionAuditLogs(std::list<AuditLog>& auditLogs,
                                   OrthancPluginResourceType resourceType,
@@ -1812,12 +1843,11 @@
   }
 
   Json::Value payload;
-  if (!OrthancPlugins::ReadJson(payload, request->body, request->bodySize) || !payload.isMember("Resources"))
-  {
-    throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "A JSON payload was expected");
-  }
-
-  if (!CheckHasAccessToAllResourcesInPayload(output, request))
+  std::map<std::string, Orthanc::ResourceType> resources;
+  
+  GetResourcesList(resources, payload, request);
+
+  if (!CheckHasAccessToAllResourcesInPayload(resources, output, request))
   {
     return;
   }
@@ -1826,11 +1856,9 @@
 
   if (enableAudiLogs)
   {
-    for (Json::ArrayIndex i = 0; i < payload["Resources"].size(); ++i)
+    for (std::map<std::string, Orthanc::ResourceType>::const_iterator it = resources.begin(); it != resources.end(); ++it)
     {
-      std::string resourceId = payload["Resources"][i].asString();
-      OrthancPluginResourceType resourceType = IdentifyResourceType(resourceId);
-      GetResourceDeletionAuditLogs(auditLogs, resourceType, resourceId, request);
+      GetResourceDeletionAuditLogs(auditLogs, Convert(it->second), it->first, request);
     }
   }