changeset 313:be8457e5aabc

fix /tool/bulk-delete: a user was able to delete resources he does not have access to
author Alain Mazy <am@orthanc.team>
date Fri, 20 Mar 2026 17:06:41 +0100
parents 773185dc4db4
children 331809444cec
files NEWS Plugin/AuthorizationParserBase.cpp Plugin/AuthorizationParserBase.h Plugin/Plugin.cpp
diffstat 4 files changed, 109 insertions(+), 9 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Tue Feb 10 14:47:00 2026 +0100
+++ b/NEWS	Fri Mar 20 17:06:41 2026 +0100
@@ -3,6 +3,8 @@
 
 * Now recording audit-logs when uploading a zip.
 * New default permissions for sending emails when sharing studies.
+* Fix: in /tools/bulk-delete, a user was able to delete resources
+  he does not have access to.
 
 
 2025-11-20 - v 0.10.3
--- a/Plugin/AuthorizationParserBase.cpp	Tue Feb 10 14:47:00 2026 +0100
+++ b/Plugin/AuthorizationParserBase.cpp	Fri Mar 20 17:06:41 2026 +0100
@@ -69,6 +69,29 @@
     throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
   }
 
+  void AuthorizationParserBase::AddOrthancResource(AccessedResources& target,
+                                                   Orthanc::ResourceType type,
+                                                   const std::string& orthancId)
+  {
+    switch (type)
+    {
+      case Orthanc::ResourceType_Instance:
+        AddOrthancInstance(target, orthancId);
+        break;
+      case Orthanc::ResourceType_Series:
+        AddOrthancSeries(target, orthancId);
+        break;
+      case Orthanc::ResourceType_Study:
+        AddOrthancStudy(target, orthancId);
+        break;
+      case Orthanc::ResourceType_Patient:
+        AddOrthancPatient(target, orthancId);
+        break;
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+  }
+
 
   void AuthorizationParserBase::AddOrthancInstance(AccessedResources& target,
                                                    const std::string& orthancId)
--- a/Plugin/AuthorizationParserBase.h	Tue Feb 10 14:47:00 2026 +0100
+++ b/Plugin/AuthorizationParserBase.h	Fri Mar 20 17:06:41 2026 +0100
@@ -55,9 +55,6 @@
     void AddOrthancPatient(AccessedResources& target,
                            const std::string& orthancId);
 
-    Orthanc::ResourceType AddOrthancUnknownResource(AccessedResources& target,
-                                                    const std::string& orthancId);
-
     void AddDicomPatient(AccessedResources& target,
                          const std::string& patientId);
 
@@ -71,6 +68,13 @@
                           const std::string& instanceDicomUid);
 
   public:
+    Orthanc::ResourceType AddOrthancUnknownResource(AccessedResources& target,
+                                                    const std::string& orthancId);
+
+    void AddOrthancResource(AccessedResources& target,
+                            Orthanc::ResourceType type,
+                            const std::string& orthancId);
+
     virtual void AddDicomStudy(AccessedResources& target,
                                const std::string& studyDicomUid) ORTHANC_OVERRIDE;
 
--- a/Plugin/Plugin.cpp	Tue Feb 10 14:47:00 2026 +0100
+++ b/Plugin/Plugin.cpp	Fri Mar 20 17:06:41 2026 +0100
@@ -40,7 +40,7 @@
 static bool resourceTokensEnabled_ = false;
 static bool userTokensEnabled_ = false;
 static bool enableAuditLogs_ = false;
-static std::unique_ptr<OrthancPlugins::IAuthorizationParser> authorizationParser_;
+static std::unique_ptr<OrthancPlugins::AuthorizationParserBase> authorizationParser_;
 static std::unique_ptr<OrthancPlugins::IAuthorizationService> authorizationService_;
 static std::unique_ptr<OrthancPlugins::PermissionParser> permissionParser_;
 static std::set<std::string> uncheckedResources_;
@@ -1021,6 +1021,67 @@
 }
 
 
+void CheckHasAccessToAllResourcesInPayload(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)
+
+  OrthancPlugins::IAuthorizationService::UserProfile profile;
+  if (!GetUserProfileInternal(profile, request))
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_ForbiddenAccess, "Auth plugin: unable to check if the user has access to all Resources in the payload - no user profile found");
+  }
+
+  if (HasAccessToAllLabels(profile))  // these guys can do whatever they want
+  {
+    return;
+  }
+
+  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' field was expected");
+  }
+
+  Orthanc::ResourceType levelInPayload = Orthanc::ResourceType_Instance; // random value
+  bool hasLevelInPayload = payload.isMember("Level") && payload["Level"].isString();
+  
+  if (hasLevelInPayload)
+  {
+    levelInPayload = Orthanc::StringToResourceType(payload["Level"].asString().c_str());
+  }
+  
+  std::map<std::string, Orthanc::ResourceType> resources;
+
+  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);
+    }
+
+    bool granted = false;
+
+    if (!HasAuthorizedLabelsForResource(granted, accessedResources, profile))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ForbiddenAccess, "Auth plugin: Unable to check resource access based on the authorized_labels.");
+    }
+
+    if (!granted)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ForbiddenAccess, "Auth plugin: the user does not have access to resource " + resourceId);
+    }
+  }
+}
+
+
 void ToolsFindOrCountResources(OrthancPluginRestOutput* output,
                                const char* /*url*/,
                                const OrthancPluginHttpRequest* request,
@@ -1653,6 +1714,7 @@
 }
 
 
+template <bool enableAudiLogs>
 void BulkDeleteWithAuditLogs(OrthancPluginRestOutput* output,
                              const char* url,
                              const OrthancPluginHttpRequest* request)
@@ -1670,13 +1732,18 @@
     throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "A JSON payload was expected");
   }
 
+  CheckHasAccessToAllResourcesInPayload(output, request);
+
   std::list<AuditLog> auditLogs;
 
-  for (Json::ArrayIndex i = 0; i < payload["Resources"].size(); ++i)
+  if (enableAudiLogs)
   {
-    std::string resourceId = payload["Resources"][i].asString();
-    OrthancPluginResourceType resourceType = IdentifyResourceType(resourceId);
-    GetResourceDeletionAuditLogs(auditLogs, resourceType, resourceId, request);
+    for (Json::ArrayIndex i = 0; i < payload["Resources"].size(); ++i)
+    {
+      std::string resourceId = payload["Resources"][i].asString();
+      OrthancPluginResourceType resourceType = IdentifyResourceType(resourceId);
+      GetResourceDeletionAuditLogs(auditLogs, resourceType, resourceId, request);
+    }
   }
 
   OrthancPlugins::RestApiClient coreApi(url, request);
@@ -2496,7 +2563,7 @@
             OrthancPlugins::RegisterRestCallback<AnonymizeWithAuditLogs>("/(patients|studies|series)/([^/]*)/anonymize", true);
             OrthancPlugins::RegisterRestCallback<ModifyWithAuditLogs>("/(patients|studies|series)/([^/]*)/modify", true);
             OrthancPlugins::RegisterRestCallback<LabelWithAuditLogs>("/(patients|studies|series)/([^/]*)/labels/([^/]*)", true);
-            OrthancPlugins::RegisterRestCallback<BulkDeleteWithAuditLogs>("/tools/bulk-delete", true);
+            OrthancPlugins::RegisterRestCallback<BulkDeleteWithAuditLogs<true> >("/tools/bulk-delete", true);
             OrthancPlugins::RegisterRestCallback<BulkModifyAnonymizeWithAuditLogs>("/tools/bulk-modify", true);
             OrthancPlugins::RegisterRestCallback<BulkModifyAnonymizeWithAuditLogs>("/tools/bulk-anonymize", true);
             OrthancPlugins::RegisterRestCallback<GetAuditLogs>("/auth/audit-logs", true);
@@ -2510,6 +2577,10 @@
             // /media + create-media + create-media-extended
 
           }
+          else
+          {
+            OrthancPlugins::RegisterRestCallback<BulkDeleteWithAuditLogs<false> >("/tools/bulk-delete", true);
+          }
         }
 
         if (!urlTokenCreationBase.empty())