changeset 246:26ca67fe0659 inbox

audit-logs first part
author Alain Mazy <am@orthanc.team>
date Mon, 07 Jul 2025 10:55:00 +0200
parents a56513c56d0d
children ff21632f3ab6
files NEWS Plugin/AuthorizationWebService.cpp Plugin/Enumerations.cpp Plugin/Enumerations.h Plugin/IAuthorizationService.h Plugin/Plugin.cpp Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h
diffstat 8 files changed, 679 insertions(+), 53 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Tue Jul 01 15:29:03 2025 +0200
+++ b/NEWS	Mon Jul 07 10:55:00 2025 +0200
@@ -13,6 +13,9 @@
   HTTP request.
 * The User profile can now contain a "groups" field if the auth-service
   provides it.
+* The User profile can now contain an "id" field if the auth-service
+  provides it.
+* New experimental feature: audit-logs (TODO)
 
 
 2025-06-11 - v 0.9.3
--- a/Plugin/AuthorizationWebService.cpp	Tue Jul 01 15:29:03 2025 +0200
+++ b/Plugin/AuthorizationWebService.cpp	Mon Jul 07 10:55:00 2025 +0200
@@ -35,6 +35,7 @@
   static const char* AUTHORIZED_LABELS = "authorized-labels";
   static const char* USER_NAME = "name";
   static const char* GROUPS = "groups";
+  static const char* USER_ID = "user-id";
 
   
 
@@ -341,6 +342,7 @@
   {
     jsonProfile = Json::objectValue;
     jsonProfile[USER_NAME] = profile.name;
+    jsonProfile[USER_ID] = profile.userId;
     Orthanc::SerializationToolbox::WriteSetOfStrings(jsonProfile, profile.authorizedLabels, AUTHORIZED_LABELS);
     Orthanc::SerializationToolbox::WriteSetOfStrings(jsonProfile, profile.permissions, PERMISSIONS);
     Orthanc::SerializationToolbox::WriteSetOfStrings(jsonProfile, profile.groups, GROUPS);
@@ -379,6 +381,11 @@
         profile.groups.insert(jsonProfile[GROUPS][i].asString());
       }
     }
+
+    if (jsonProfile.isMember(USER_ID) && jsonProfile[USER_ID].isString())
+    {
+      profile.userId = jsonProfile[USER_ID].asString();
+    }
   }
 
 
--- a/Plugin/Enumerations.cpp	Tue Jul 01 15:29:03 2025 +0200
+++ b/Plugin/Enumerations.cpp	Mon Jul 07 10:55:00 2025 +0200
@@ -84,4 +84,31 @@
       throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, std::string("Invalid access level: ") + tmp);
     }
   }
+
+  OrthancPluginResourceType StringToResourceType(const char* type)
+  {
+    std::string s(type);
+    Orthanc::Toolbox::ToUpperCase(s);
+
+    if (s == "PATIENT" || s == "PATIENTS")
+    {
+      return OrthancPluginResourceType_Patient;
+    }
+    else if (s == "STUDY" || s == "STUDIES")
+    {
+      return OrthancPluginResourceType_Study;
+    }
+    else if (s == "SERIES")
+    {
+      return OrthancPluginResourceType_Series;
+    }
+    else if (s == "INSTANCE"  || s == "IMAGE" || 
+             s == "INSTANCES" || s == "IMAGES")
+    {
+      return OrthancPluginResourceType_Instance;
+    }
+
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, std::string("Invalid resource type '") + type + "'");
+  }
+
 }
--- a/Plugin/Enumerations.h	Tue Jul 01 15:29:03 2025 +0200
+++ b/Plugin/Enumerations.h	Mon Jul 07 10:55:00 2025 +0200
@@ -21,6 +21,7 @@
 #pragma once
 
 #include <Enumerations.h>
+#include <orthanc/OrthancCPlugin.h>
 
 namespace OrthancPlugins
 {
@@ -46,4 +47,6 @@
   std::string EnumerationToString(AccessLevel level);
 
   AccessLevel StringToAccessLevel(const std::string& level);
+
+  OrthancPluginResourceType StringToResourceType(const char* type);
 }
--- a/Plugin/IAuthorizationService.h	Tue Jul 01 15:29:03 2025 +0200
+++ b/Plugin/IAuthorizationService.h	Mon Jul 07 10:55:00 2025 +0200
@@ -61,6 +61,7 @@
     struct UserProfile
     {
       std::string name;
+      std::string userId;
       std::set<std::string> permissions;
       std::set<std::string> authorizedLabels;
       std::set<std::string> groups;
--- a/Plugin/Plugin.cpp	Tue Jul 01 15:29:03 2025 +0200
+++ b/Plugin/Plugin.cpp	Mon Jul 07 10:55:00 2025 +0200
@@ -38,6 +38,7 @@
 // Configuration of the authorization plugin
 static bool resourceTokensEnabled_ = false;
 static bool userTokensEnabled_ = false;
+static bool enableAuditLogs_ = true;
 static std::unique_ptr<OrthancPlugins::IAuthorizationParser> authorizationParser_;
 static std::unique_ptr<OrthancPlugins::IAuthorizationService> authorizationService_;
 static std::unique_ptr<OrthancPlugins::PermissionParser> permissionParser_;
@@ -64,6 +65,37 @@
   OrthancPluginSendHttpStatus(context, output, 403, message, strlen(message));
 }
 
+static const char* KEY_USER_DATA = "UserData";
+static const char* KEY_USER_ID = "AuditLogsUserId";
+
+static bool GetUserIdFromUserData(std::string& userId, const Json::Value& payload)
+{
+  if (payload.isMember(KEY_USER_DATA) && payload[KEY_USER_DATA].isObject() 
+    && payload[KEY_USER_DATA].isMember(KEY_USER_ID) && payload[KEY_USER_DATA][KEY_USER_ID].isString())
+  {
+    userId = payload[KEY_USER_DATA][KEY_USER_ID].asString();
+    return true;
+  }
+  return false;
+}
+
+static void SetUserIdInUserdata(Json::Value& payload, const std::string& userId)
+{
+  if (!payload.isMember(KEY_USER_DATA))
+  {
+    payload[KEY_USER_DATA] = Json::objectValue;
+  }
+  payload[KEY_USER_DATA][KEY_USER_ID] = userId;
+}
+
+static void RecordAuditLog(const std::string& userId,
+                           const OrthancPluginResourceType& resourceType,
+                           const std::string& resourceId,
+                           const std::string& action,
+                           const Json::Value& logData)
+{
+  LOG(WARNING) << "AUDIT-LOG: " << userId << " / " << action << " on " << resourceType << ":" << resourceId << ", " << logData.toStyledString();
+}
 
 
 class TokenAndValue
@@ -495,34 +527,96 @@
 {
   try
   {
-    if (authorizationParser_.get() == NULL)
+    if (authorizationParser_.get() == NULL || !enableAuditLogs_)
     {
       return OrthancPluginErrorCode_Success;
     }
-    
-    if (changeType == OrthancPluginChangeType_Deleted)
+
+    switch(changeType)
     {
-      switch (resourceType)
+      case OrthancPluginChangeType_JobSuccess:
       {
-        case OrthancPluginResourceType_Patient:
-          authorizationParser_->Invalidate(Orthanc::ResourceType_Patient, resourceId);
-          break;
+        Json::Value job;
+        if (OrthancPlugins::RestApiGet(job, std::string("/jobs/") + resourceId, false))
+        {
+          if (job["Type"].asString() == "ResourceModification")
+          {
+            Json::Value jobContent = job["Content"];
+            std::string sourceResourceId = jobContent["ParentResources"][0].asString();
+            std::string modifiedResourceId = jobContent["ID"].asString();
+            OrthancPluginResourceType resourceType = OrthancPlugins::StringToResourceType(jobContent["Type"].asString().c_str());
+
+            bool isAnonymization = jobContent.isMember("IsAnonymization") && jobContent["IsAnonymization"].asBool();
+            LOG(WARNING) << jobContent.toStyledString();
+
+            if (isAnonymization)
+            {
+              std::string userId;
+              if (GetUserIdFromUserData(userId, job))
+              {
+                // attach a log to the source study
+                Json::Value logData;
+                logData["ModifiedResourceId"] = modifiedResourceId;
+                logData["ModifiedResourceType"] = resourceType;
+
+                RecordAuditLog(userId, 
+                               resourceType,
+                               sourceResourceId, 
+                               (isAnonymization ? "success-anonymization" : "success-modification-job"), 
+                               logData);
+                
+                // attach a log to the modified study
+                if (sourceResourceId != modifiedResourceId)
+                {
+                  Json::Value logData;
+                  logData["SourceResourceId"] = sourceResourceId;
+                  logData["SourceResourceType"] = resourceType;
 
-        case OrthancPluginResourceType_Study:
-          authorizationParser_->Invalidate(Orthanc::ResourceType_Study, resourceId);
-          break;
+                  RecordAuditLog(userId, 
+                                 resourceType,
+                                 modifiedResourceId, 
+                                 (isAnonymization ? "new-study-from-anonymization-job" : "new-study-from-modification-job"), 
+                                 logData);
+                }
+              }
+            }            
+          }
 
-        case OrthancPluginResourceType_Series:
-          authorizationParser_->Invalidate(Orthanc::ResourceType_Series, resourceId);
-          break;
+        }
+
+        return OrthancPluginErrorCode_Success;
+      }
+      case OrthancPluginChangeType_JobFailure:
+      {
+        return OrthancPluginErrorCode_Success;
+      }
 
-        case OrthancPluginResourceType_Instance:
-          authorizationParser_->Invalidate(Orthanc::ResourceType_Instance, resourceId);
-          break;
+      case OrthancPluginChangeType_Deleted:
+      {
+        switch (resourceType)
+        {
+          case OrthancPluginResourceType_Patient:
+            authorizationParser_->Invalidate(Orthanc::ResourceType_Patient, resourceId);
+            break;
+
+          case OrthancPluginResourceType_Study:
+            authorizationParser_->Invalidate(Orthanc::ResourceType_Study, resourceId);
+            break;
 
-        default:
-          break;
+          case OrthancPluginResourceType_Series:
+            authorizationParser_->Invalidate(Orthanc::ResourceType_Series, resourceId);
+            break;
+
+          case OrthancPluginResourceType_Instance:
+            authorizationParser_->Invalidate(Orthanc::ResourceType_Instance, resourceId);
+            break;
+
+          default:
+            break;
+        }
       }
+      default:
+        return OrthancPluginErrorCode_Success;    
     }
        
     return OrthancPluginErrorCode_Success;
@@ -952,6 +1046,109 @@
   ToolsFindOrCountResources(output, url, request, "/tools/count-resources", false);
 }
 
+void UploadInstancesWithAuditLogs(OrthancPluginRestOutput* output,
+                                  const char* url,
+                                  const OrthancPluginHttpRequest* request)
+{
+  OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
+
+  // always forward to core
+  OrthancPlugins::RestApiClient coreApi(url, request);
+  coreApi.Forward(context, output);
+
+  if (request->method == OrthancPluginHttpMethod_Post)
+  {
+    OrthancPlugins::IAuthorizationService::UserProfile profile;
+    Json::Value coreResponse;
+
+    if (GetUserProfileInternal(profile, request) && coreApi.GetAnswerJson(coreResponse))
+    {
+      RecordAuditLog(profile.userId, OrthancPluginResourceType_Study, coreResponse["ParentStudy"].asString(), "uploaded-instance", std::string());
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ForbiddenAccess, "Auth plugin: no user profile found, unable to handle POST to /instances with audit logs enabled.");
+    }
+  }
+}
+
+
+void ModifyAnonymizeWithAuditLogs(OrthancPluginRestOutput* output,
+                                  const char* url,
+                                  const OrthancPluginHttpRequest* request,
+                                  bool isModification)
+{
+  OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
+  OrthancPluginResourceType resourceType = OrthancPlugins::StringToResourceType(request->groups[0]);
+  std::string resourceId = request->groups[1];
+
+  OrthancPlugins::RestApiClient coreApi(url, request);
+
+  Json::Value payload;
+  if (!OrthancPlugins::ReadJson(payload, request->body, request->bodySize))
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "A JSON payload was expected");
+  }
+
+  // Either there is a userId in UserData or the request comes from a user with profile
+  OrthancPlugins::IAuthorizationService::UserProfile profile;
+  std::string userId;
+
+  LOG(WARNING) << payload.toStyledString();
+
+  if (GetUserProfileInternal(profile, request) && !profile.userId.empty())
+  {
+    userId = profile.userId;  
+  } 
+  else if (!GetUserIdFromUserData(userId, payload))
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_ForbiddenAccess, "Auth plugin: no user profile or UserData found, unable to handle anonymize/modify with audit logs enabled.");
+  }
+
+  if ((payload.isMember("Synchronous") && !payload["Synchronous"].asBool())
+    || (payload.isMember("Asynchronous") && !payload["Asynchronous"].asBool()))
+  {
+    // add UserData to the job payload to know who has modified the data.  The handling of the log will then happen in the OnChange handler
+    SetUserIdInUserdata(payload, userId);
+
+    // in any case, record that this resource is being modified/anonymized and record the payload
+    RecordAuditLog(userId, 
+                   resourceType, 
+                   resourceId, 
+                   (isModification ? "start-modification-job" : "start-anonymization-job"), 
+                   payload);
+
+    coreApi.Forward(context, output);
+  }
+  else
+  {
+    Json::Value coreResponse;
+
+    // if it is synchronous, perform the modification and record the log directly
+    coreApi.Forward(context, output);
+    
+    if (coreApi.GetAnswerJson(coreResponse))
+    {
+      LOG(WARNING) << "TODO AUDIT-LOG " << coreResponse.toStyledString(); // TODO 
+    }
+  }
+}
+
+void ModifyWithAuditLogs(OrthancPluginRestOutput* output,
+                         const char* url,
+                         const OrthancPluginHttpRequest* request)
+{
+  ModifyAnonymizeWithAuditLogs(output, url, request, true);
+}
+
+void AnonymizeWithAuditLogs(OrthancPluginRestOutput* output,
+                            const char* url,
+                            const OrthancPluginHttpRequest* request)
+{
+  ModifyAnonymizeWithAuditLogs(output, url, request, false);
+}
+
+
 void ToolsLabels(OrthancPluginRestOutput* output,
                  const char* /*url*/,
                  const OrthancPluginHttpRequest* request)
@@ -1262,6 +1459,11 @@
         jsonProfile["groups"].append(*it);
       }
 
+      if (!profile.userId.empty())
+      {
+        jsonProfile["user-id"] = profile.userId;
+      }
+
       OrthancPlugins::AnswerJson(jsonProfile, output);
     }
   }
@@ -1706,7 +1908,7 @@
           (new OrthancPlugins::CachedAuthorizationService
            (webService.release(), factory));
 
-        if (!urlTokenValidation.empty())
+        if (!urlTokenValidation.empty() || enableAuditLogs_)
         {
           OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback);
         }
@@ -1747,6 +1949,13 @@
           OrthancPlugins::RegisterRestCallback<FilterLabelsFromResourceList>("/patients/([^/]*)/instances", true);
           OrthancPlugins::RegisterRestCallback<FilterLabelsFromResourceList>("/patients/([^/]*)/series", true);
           OrthancPlugins::RegisterRestCallback<FilterLabelsFromResourceList>("/patients/([^/]*)/studies", true);
+
+          if (enableAuditLogs_)
+          {
+            OrthancPlugins::RegisterRestCallback<UploadInstancesWithAuditLogs>("/instances", true);
+            OrthancPlugins::RegisterRestCallback<AnonymizeWithAuditLogs>("/(patients|studies|series)/([^/]*)/anonymize", true);
+            OrthancPlugins::RegisterRestCallback<ModifyWithAuditLogs>("/(patients|studies|series)/([^/]*)/modify", true);
+          }
         }
 
         if (!urlTokenCreationBase.empty())
--- a/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp	Tue Jul 01 15:29:03 2025 +0200
+++ b/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp	Mon Jul 07 10:55:00 2025 +0200
@@ -220,28 +220,6 @@
   }
 
 
-#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
-  MemoryBuffer::MemoryBuffer(const void* buffer,
-                             size_t size)
-  {
-    uint32_t s = static_cast<uint32_t>(size);
-    if (static_cast<size_t>(s) != size)
-    {
-      ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory);
-    }
-    else if (OrthancPluginCreateMemoryBuffer(GetGlobalContext(), &buffer_, s) !=
-             OrthancPluginErrorCode_Success)
-    {
-      ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory);
-    }
-    else
-    {
-      memcpy(buffer_.data, buffer, size);
-    }
-  }
-#endif
-
-
   void MemoryBuffer::Clear()
   {
     if (buffer_.data != NULL)
@@ -253,6 +231,41 @@
   }
 
 
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
+  void MemoryBuffer::Assign(const void* buffer,
+                            size_t size)
+  {
+    uint32_t s = static_cast<uint32_t>(size);
+    if (static_cast<size_t>(s) != size)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory);
+    }
+
+    Clear();
+
+    if (OrthancPluginCreateMemoryBuffer(GetGlobalContext(), &buffer_, s) !=
+        OrthancPluginErrorCode_Success)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory);
+    }
+    else
+    {
+      if (size > 0)
+      {
+        memcpy(buffer_.data, buffer, size);
+      }
+    }
+  }
+#endif
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
+  void MemoryBuffer::Assign(const std::string& s)
+  {
+    Assign(s.empty() ? NULL : s.c_str(), s.size());
+  }
+#endif
+
+
   void MemoryBuffer::Assign(OrthancPluginMemoryBuffer& other)
   {
     Clear();
@@ -673,7 +686,7 @@
   {
     OrthancString str;
     str.Assign(OrthancPluginDicomBufferToJson
-               (GetGlobalContext(), GetData(), GetSize(), format, flags, maxStringLength));
+               (GetGlobalContext(), reinterpret_cast<const char*>(GetData()), GetSize(), format, flags, maxStringLength));
     str.ToJson(target);
   }
 
@@ -1566,7 +1579,7 @@
     {
       if (!answer.IsEmpty())
       {
-        result.assign(answer.GetData(), answer.GetSize());
+        result.assign(reinterpret_cast<const char*>(answer.GetData()), answer.GetSize());
       }
       return true;
     }
@@ -2052,6 +2065,26 @@
             DoPost(target, index, uri, body, headers));
   }
 
+  bool OrthancPeers::DoPost(Json::Value& target,
+                            size_t index,
+                            const std::string& uri,
+                            const std::string& body,
+                            const HttpHeaders& headers, 
+                            unsigned int timeout) const
+  {
+    MemoryBuffer buffer;
+
+    if (DoPost(buffer, index, uri, body, headers, timeout))
+    {
+      buffer.ToJson(target);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
 
   bool OrthancPeers::DoPost(Json::Value& target,
                             size_t index,
@@ -2099,6 +2132,17 @@
                             const std::string& body,
                             const HttpHeaders& headers) const
   {
+    return DoPost(target, index, uri, body, headers, timeout_);
+  }
+
+
+  bool OrthancPeers::DoPost(MemoryBuffer& target,
+                            size_t index,
+                            const std::string& uri,
+                            const std::string& body,
+                            const HttpHeaders& headers,
+                            unsigned int timeout) const
+  {
     if (index >= index_.size())
     {
       ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange);
@@ -2117,7 +2161,7 @@
     OrthancPluginErrorCode code = OrthancPluginCallPeerApi
       (GetGlobalContext(), *answer, NULL, &status, peers_,
        static_cast<uint32_t>(index), OrthancPluginHttpMethod_Post, uri.c_str(),
-       pluginHeaders.GetSize(), pluginHeaders.GetKeys(), pluginHeaders.GetValues(), body.empty() ? NULL : body.c_str(), body.size(), timeout_);
+       pluginHeaders.GetSize(), pluginHeaders.GetKeys(), pluginHeaders.GetValues(), body.empty() ? NULL : body.c_str(), body.size(), timeout);
 
     if (code == OrthancPluginErrorCode_Success)
     {
@@ -4168,6 +4212,11 @@
     {
       path_ += "?" + getArguments;
     }
+
+    if (request->bodySize > 0 && request->body != NULL)
+    {
+      requestBody_.assign(reinterpret_cast<const char*>(request->body), request->bodySize);
+    }
   }
 #endif
 
@@ -4316,4 +4365,209 @@
     }
   }
 #endif
+
+
+#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1
+  KeyValueStore::Iterator::Iterator(OrthancPluginKeysValuesIterator  *iterator) :
+    iterator_(iterator)
+  {
+    if (iterator_ == NULL)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
+    }
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1
+  KeyValueStore::Iterator::~Iterator()
+  {
+    OrthancPluginFreeKeysValuesIterator(OrthancPlugins::GetGlobalContext(), iterator_);
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1
+  bool KeyValueStore::Iterator::Next()
+  {
+    uint8_t done;
+    OrthancPluginErrorCode code = OrthancPluginKeysValuesIteratorNext(OrthancPlugins::GetGlobalContext(), &done, iterator_);
+
+    if (code != OrthancPluginErrorCode_Success)
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+    }
+    else
+    {
+      return (done != 0);
+    }
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1
+  std::string KeyValueStore::Iterator::GetKey() const
+  {
+    const char* s = OrthancPluginKeysValuesIteratorGetKey(OrthancPlugins::GetGlobalContext(), iterator_);
+    if (s == NULL)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
+    }
+    else
+    {
+      return s;
+    }
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1
+  void KeyValueStore::Iterator::GetValue(std::string& value) const
+  {
+    OrthancPlugins::MemoryBuffer valueBuffer;
+    OrthancPluginErrorCode code = OrthancPluginKeysValuesIteratorGetValue(OrthancPlugins::GetGlobalContext(), *valueBuffer, iterator_);
+
+    if (code != OrthancPluginErrorCode_Success)
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+    }
+    else
+    {
+      valueBuffer.ToString(value);
+    }
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1
+  void KeyValueStore::Store(const std::string& key,
+                            const void* value,
+                            size_t valueSize)
+  {
+    if (static_cast<size_t>(static_cast<uint32_t>(valueSize)) != valueSize)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory);
+    }
+
+    OrthancPluginErrorCode code = OrthancPluginStoreKeyValue(OrthancPlugins::GetGlobalContext(), storeId_.c_str(),
+                                                             key.c_str(), value, static_cast<uint32_t>(valueSize));
+    if (code != OrthancPluginErrorCode_Success)
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+    }
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1
+  bool KeyValueStore::GetValue(std::string& value,
+                               const std::string& key)
+  {
+    uint8_t found = false;
+    OrthancPlugins::MemoryBuffer valueBuffer;
+    OrthancPluginErrorCode code = OrthancPluginGetKeyValue(OrthancPlugins::GetGlobalContext(), &found,
+                                                           *valueBuffer, storeId_.c_str(), key.c_str());
+
+    if (code != OrthancPluginErrorCode_Success)
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+    }
+    else if (found)
+    {
+      valueBuffer.ToString(value);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1
+  void KeyValueStore::DeleteKey(const std::string& key)
+  {
+    OrthancPluginErrorCode code = OrthancPluginDeleteKeyValue(OrthancPlugins::GetGlobalContext(),
+                                                              storeId_.c_str(), key.c_str());
+
+    if (code != OrthancPluginErrorCode_Success)
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+    }
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1
+  KeyValueStore::Iterator* KeyValueStore::CreateIterator()
+  {
+    return new Iterator(OrthancPluginCreateKeysValuesIterator(OrthancPlugins::GetGlobalContext(), storeId_.c_str()));
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_QUEUES == 1
+  void Queue::Enqueue(const void* value,
+                      size_t valueSize)
+  {
+    if (static_cast<size_t>(static_cast<uint32_t>(valueSize)) != valueSize)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory);
+    }
+
+    OrthancPluginErrorCode code = OrthancPluginEnqueueValue(OrthancPlugins::GetGlobalContext(),
+                                                            queueId_.c_str(), value, static_cast<uint32_t>(valueSize));
+
+    if (code != OrthancPluginErrorCode_Success)
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+    }
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_QUEUES == 1
+  bool Queue::DequeueInternal(std::string& value,
+                              OrthancPluginQueueOrigin origin)
+  {
+    uint8_t found = false;
+    OrthancPlugins::MemoryBuffer valueBuffer;
+
+    OrthancPluginErrorCode code = OrthancPluginDequeueValue(OrthancPlugins::GetGlobalContext(), &found,
+                                                            *valueBuffer, queueId_.c_str(), origin);
+
+    if (code != OrthancPluginErrorCode_Success)
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+    }
+    else if (found)
+    {
+      valueBuffer.ToString(value);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_QUEUES == 1
+  uint64_t Queue::GetSize()
+  {
+    uint64_t size = 0;
+    OrthancPluginErrorCode code = OrthancPluginGetQueueSize(OrthancPlugins::GetGlobalContext(), queueId_.c_str(), &size);
+
+    if (code != OrthancPluginErrorCode_Success)
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+    }
+    else
+    {
+      return size;
+    }
+  }
+#endif
 }
--- a/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h	Tue Jul 01 15:29:03 2025 +0200
+++ b/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h	Mon Jul 07 10:55:00 2025 +0200
@@ -134,6 +134,14 @@
 #  define HAS_ORTHANC_PLUGIN_LOG_MESSAGE  0
 #endif
 
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 8)
+#  define HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES  1
+#  define HAS_ORTHANC_PLUGIN_QUEUES            1
+#else
+#  define HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES  0
+#  define HAS_ORTHANC_PLUGIN_QUEUES            0
+#endif
+
 
 // Macro to tag a function as having been deprecated
 #if (__cplusplus >= 201402L)  // C++14
@@ -203,13 +211,6 @@
   public:
     MemoryBuffer();
 
-#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
-    // This constructor makes a copy of the given buffer in the memory
-    // handled by the Orthanc core
-    MemoryBuffer(const void* buffer,
-                 size_t size);
-#endif
-
     ~MemoryBuffer()
     {
       Clear();
@@ -220,6 +221,16 @@
       return &buffer_;
     }
 
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
+    // Copy of the given buffer into the memory managed by the Orthanc core
+    void Assign(const void* buffer,
+                size_t size);
+#endif
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
+    void Assign(const std::string& s);
+#endif
+
     // This transfers ownership from "other" to "this"
     void Assign(OrthancPluginMemoryBuffer& other);
 
@@ -227,11 +238,11 @@
 
     OrthancPluginMemoryBuffer Release();
 
-    const char* GetData() const
+    const void* GetData() const
     {
       if (buffer_.size > 0)
       {
-        return reinterpret_cast<const char*>(buffer_.data);
+        return buffer_.data;
       }
       else
       {
@@ -855,6 +866,13 @@
                 const HttpHeaders& headers) const;
 
     bool DoPost(MemoryBuffer& target,
+                size_t index,
+                const std::string& uri,
+                const std::string& body,
+                const HttpHeaders& headers,
+                unsigned int timeout) const;
+
+    bool DoPost(MemoryBuffer& target,
                 const std::string& name,
                 const std::string& uri,
                 const std::string& body,
@@ -867,6 +885,13 @@
                 const HttpHeaders& headers) const;
 
     bool DoPost(Json::Value& target,
+                size_t index,
+                const std::string& uri,
+                const std::string& body,
+                const HttpHeaders& headers,
+                unsigned int timeout) const;
+
+    bool DoPost(Json::Value& target,
                 const std::string& name,
                 const std::string& uri,
                 const std::string& body,
@@ -1604,4 +1629,101 @@
     bool GetAnswerJson(Json::Value& output) const;
   };
 #endif
+
+
+#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1
+  class KeyValueStore : public boost::noncopyable
+  {
+  public:
+    class Iterator : public boost::noncopyable
+    {
+    private:
+      OrthancPluginKeysValuesIterator  *iterator_;
+
+    public:
+      Iterator(OrthancPluginKeysValuesIterator  *iterator);
+
+      ~Iterator();
+
+      bool Next();
+
+      std::string GetKey() const;
+
+      void GetValue(std::string& target) const;
+    };
+
+  private:
+    std::string storeId_;
+
+  public:
+    explicit KeyValueStore(const std::string& storeId) :
+      storeId_(storeId)
+    {
+    }
+
+    const std::string& GetStoreId() const
+    {
+      return storeId_;
+    }
+
+    void Store(const std::string& key,
+               const void* value,
+               size_t valueSize);
+
+    void Store(const std::string& key,
+               const std::string& value)
+    {
+      Store(key, value.empty() ? NULL : value.c_str(), value.size());
+    }
+
+    bool GetValue(std::string& value,
+                  const std::string& key);
+
+    void DeleteKey(const std::string& key);
+
+    Iterator* CreateIterator();
+  };
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_QUEUES == 1
+  class Queue : public boost::noncopyable
+  {
+  private:
+    std::string queueId_;
+
+    bool DequeueInternal(std::string& value, OrthancPluginQueueOrigin origin);
+
+  public:
+    explicit Queue(const std::string& queueId) :
+      queueId_(queueId)
+    {
+    }
+
+    const std::string& GetQueueId() const
+    {
+      return queueId_;
+    }
+
+    void Enqueue(const void* value,
+                 size_t valueSize);
+
+    void Enqueue(const std::string& value)
+    {
+      Enqueue(value.empty() ? NULL : value.c_str(), value.size());
+    }
+
+    bool DequeueBack(std::string& value)
+    {
+      return DequeueInternal(value, OrthancPluginQueueOrigin_Back);
+    }
+
+    bool DequeueFront(std::string& value)
+    {
+      return DequeueInternal(value, OrthancPluginQueueOrigin_Front);
+    }
+
+    uint64_t GetSize();
+  };
+#endif
 }