changeset 5273:7cb1b851f5c8

Added a sample plugin bringing multitenant DICOM support through labels
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 14 Apr 2023 11:49:24 +0200
parents a45e8b6115f6
children e5b0bd6b2242
files NEWS OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp OrthancFramework/Sources/Toolbox.cpp OrthancServer/CMakeLists.txt OrthancServer/Plugins/Samples/MultitenantDicom/CMakeLists.txt OrthancServer/Plugins/Samples/MultitenantDicom/DicomFilter.cpp OrthancServer/Plugins/Samples/MultitenantDicom/DicomFilter.h OrthancServer/Plugins/Samples/MultitenantDicom/FindRequestHandler.cpp OrthancServer/Plugins/Samples/MultitenantDicom/FindRequestHandler.h OrthancServer/Plugins/Samples/MultitenantDicom/MoveRequestHandler.cpp OrthancServer/Plugins/Samples/MultitenantDicom/MoveRequestHandler.h OrthancServer/Plugins/Samples/MultitenantDicom/MultitenantDicomServer.cpp OrthancServer/Plugins/Samples/MultitenantDicom/MultitenantDicomServer.h OrthancServer/Plugins/Samples/MultitenantDicom/NOTES.txt OrthancServer/Plugins/Samples/MultitenantDicom/OrthancFrameworkDependencies.cpp OrthancServer/Plugins/Samples/MultitenantDicom/Plugin.cpp OrthancServer/Plugins/Samples/MultitenantDicom/PluginEnumerations.h OrthancServer/Plugins/Samples/MultitenantDicom/PluginToolbox.cpp OrthancServer/Plugins/Samples/MultitenantDicom/PluginToolbox.h OrthancServer/Plugins/Samples/MultitenantDicom/StoreRequestHandler.cpp OrthancServer/Plugins/Samples/MultitenantDicom/StoreRequestHandler.h
diffstat 21 files changed, 1869 insertions(+), 6 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Thu Apr 13 21:17:55 2023 +0200
+++ b/NEWS	Fri Apr 14 11:49:24 2023 +0200
@@ -5,6 +5,7 @@
 -------
 
 * Support for labels associated with patients, studies, series, and instances
+* Added a sample plugin bringing multitenant DICOM support through labels
 
 REST API
 --------
--- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp	Thu Apr 13 21:17:55 2023 +0200
+++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp	Fri Apr 14 11:49:24 2023 +0200
@@ -93,7 +93,9 @@
 #endif
 
 #if DCMTK_USE_EMBEDDED_DICTIONARIES == 1
-#  include <OrthancFrameworkResources.h>
+#  if !defined(ORTHANC_FRAMEWORK_INCLUDE_RESOURCES) || (ORTHANC_FRAMEWORK_INCLUDE_RESOURCES == 1)
+#    include <OrthancFrameworkResources.h>
+#  endif
 #endif
 
 #if ORTHANC_ENABLE_DCMTK_JPEG == 1
--- a/OrthancFramework/Sources/Toolbox.cpp	Thu Apr 13 21:17:55 2023 +0200
+++ b/OrthancFramework/Sources/Toolbox.cpp	Fri Apr 14 11:49:24 2023 +0200
@@ -149,7 +149,9 @@
 #if defined(ORTHANC_STATIC_ICU)
 
 #  if (ORTHANC_STATIC_ICU == 1) && (ORTHANC_ENABLE_ICU == 1)
-#    include <OrthancFrameworkResources.h>
+#    if !defined(ORTHANC_FRAMEWORK_INCLUDE_RESOURCES) || (ORTHANC_FRAMEWORK_INCLUDE_RESOURCES == 1)
+#      include <OrthancFrameworkResources.h>
+#    endif
 #  endif
 
 #  if (ORTHANC_STATIC_ICU == 1 && ORTHANC_ENABLE_LOCALE == 1)
--- a/OrthancServer/CMakeLists.txt	Thu Apr 13 21:17:55 2023 +0200
+++ b/OrthancServer/CMakeLists.txt	Fri Apr 14 11:49:24 2023 +0200
@@ -61,6 +61,7 @@
 SET(BUILD_CONNECTIVITY_CHECKS ON CACHE BOOL "Whether to build the ConnectivityChecks plugin")
 SET(BUILD_HOUSEKEEPER ON CACHE BOOL "Whether to build the Housekeeper plugin")
 SET(BUILD_DELAYED_DELETION ON CACHE BOOL "Whether to build the DelayedDeletion plugin")
+SET(BUILD_MULTITENANT_DICOM ON CACHE BOOL "Whether to build the MultitenantDicom plugin")
 SET(ENABLE_PLUGINS ON CACHE BOOL "Enable plugins")
 SET(UNIT_TESTS_WITH_HTTP_CONNEXIONS ON CACHE BOOL "Allow unit tests to make HTTP requests")
 
@@ -466,12 +467,12 @@
 
 
 #####################################################################
-## Build a static library to share code between the plugins
+## Static library to share third-party libraries between the plugins
 #####################################################################
 
 if (ENABLE_PLUGINS AND
     (BUILD_SERVE_FOLDERS OR BUILD_MODALITY_WORKLISTS OR BUILD_HOUSEKEEPER OR
-      BUILD_DELAYED_DELETION))
+      BUILD_DELAYED_DELETION OR BUILD_MULTITENANT_DICOM))
   set(PLUGINS_DEPENDENCIES_SOURCES
     ${BOOST_SOURCES}
     ${JSONCPP_SOURCES}
@@ -488,8 +489,16 @@
 
   if (BUILD_DELAYED_DELETION)
     list(APPEND PLUGINS_DEPENDENCIES_SOURCES
+      ${SQLITE_SOURCES}
+      )
+  endif()
+  
+  if (BUILD_MULTITENANT_DICOM)
+    list(APPEND PLUGINS_DEPENDENCIES_SOURCES
       ${DCMTK_SOURCES}
-      ${SQLITE_SOURCES}
+      ${OPENSSL_SOURCES}
+      ${LIBJPEG_SOURCES}
+      ${LIBPNG_SOURCES}
       )
   endif()
   
@@ -642,6 +651,7 @@
   add_library(ConnectivityChecks SHARED 
     ${AUTOGENERATED_DIR}/ConnectivityChecksResources.cpp
     ${CMAKE_SOURCE_DIR}/Plugins/Samples/ConnectivityChecks/Plugin.cpp
+    
     ${CMAKE_SOURCE_DIR}/Plugins/Samples/ConnectivityChecks/OrthancFrameworkDependencies.cpp
     ${CONNECTIVITY_CHECKS_RESOURCES}
     )
@@ -693,11 +703,12 @@
   add_library(DelayedDeletion SHARED 
     ${CMAKE_SOURCE_DIR}/Plugins/Samples/DelayedDeletion/PendingDeletionsDatabase.cpp
     ${CMAKE_SOURCE_DIR}/Plugins/Samples/DelayedDeletion/Plugin.cpp
+    
     ${CMAKE_SOURCE_DIR}/Plugins/Samples/DelayedDeletion/OrthancFrameworkDependencies.cpp
     ${DELAYED_DELETION_RESOURCES}
     )
   
-  target_link_libraries(DelayedDeletion PluginsDependencies ${DCMTK_LIBRARIES})
+  target_link_libraries(DelayedDeletion PluginsDependencies)
   
   set_target_properties(
     DelayedDeletion PROPERTIES
@@ -761,6 +772,71 @@
 
 
 #####################################################################
+## Build the "MultitenantDicom" plugin
+#####################################################################
+
+if (ENABLE_PLUGINS AND BUILD_MULTITENANT_DICOM)
+  if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
+    execute_process(
+      COMMAND 
+      ${PYTHON_EXECUTABLE} ${CMAKE_SOURCE_DIR}/../OrthancFramework/Resources/WindowsResources.py
+      ${ORTHANC_VERSION} MultitenantDicom MultitenantDicom.dll "Orthanc plugin to provide a multitenant DICOM server"
+      ERROR_VARIABLE Failure
+      OUTPUT_FILE ${AUTOGENERATED_DIR}/MultitenantDicom.rc
+      )
+    
+    if (Failure)
+      message(FATAL_ERROR "Error while computing the version information: ${Failure}")
+    endif()
+    
+    list(APPEND MULTITENANT_DICOM_RESOURCES ${AUTOGENERATED_DIR}/MultitenantDicom.rc)
+  endif()
+
+  EmbedResources(
+    --target=MultitenantDicomResources
+    --namespace=Orthanc.FrameworkResources
+    --framework-path=${CMAKE_SOURCE_DIR}/../OrthancFramework/Sources
+    ${DCMTK_DICTIONARIES}
+    )
+
+  set_source_files_properties(
+    ${CMAKE_SOURCE_DIR}/Plugins/Samples/MultitenantDicom/Plugin.cpp
+    PROPERTIES COMPILE_DEFINITIONS "ORTHANC_PLUGIN_VERSION=\"${ORTHANC_VERSION}\""
+    )
+
+  # The "OrthancFrameworkDependencies.cpp" file is used to bypass the
+  # precompiled headers if compiling with Visual Studio
+  add_library(MultitenantDicom SHARED 
+    ${CMAKE_SOURCE_DIR}/Plugins/Samples/MultitenantDicom/DicomFilter.cpp
+    ${CMAKE_SOURCE_DIR}/Plugins/Samples/MultitenantDicom/FindRequestHandler.cpp
+    ${CMAKE_SOURCE_DIR}/Plugins/Samples/MultitenantDicom/MoveRequestHandler.cpp
+    ${CMAKE_SOURCE_DIR}/Plugins/Samples/MultitenantDicom/MultitenantDicomServer.cpp
+    ${CMAKE_SOURCE_DIR}/Plugins/Samples/MultitenantDicom/Plugin.cpp
+    ${CMAKE_SOURCE_DIR}/Plugins/Samples/MultitenantDicom/PluginToolbox.cpp
+    ${CMAKE_SOURCE_DIR}/Plugins/Samples/MultitenantDicom/StoreRequestHandler.cpp    
+
+    ${CMAKE_SOURCE_DIR}/Plugins/Samples/MultitenantDicom/OrthancFrameworkDependencies.cpp
+    ${AUTOGENERATED_DIR}/MultitenantDicomResources.cpp
+    ${MULTITENANT_DICOM_RESOURCES}
+    )
+  
+  target_link_libraries(MultitenantDicom PluginsDependencies ${DCMTK_LIBRARIES})
+  
+  set_target_properties(
+    MultitenantDicom PROPERTIES
+    VERSION ${ORTHANC_VERSION}
+    SOVERSION ${ORTHANC_VERSION}
+    )
+  
+  install(
+    TARGETS MultitenantDicom
+    RUNTIME DESTINATION lib    # Destination for Windows
+    LIBRARY DESTINATION share/orthanc/plugins    # Destination for Linux
+    )
+endif()
+
+
+#####################################################################
 ## Build the companion tool to recover files compressed using Orthanc
 #####################################################################
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/CMakeLists.txt	Fri Apr 14 11:49:24 2023 +0200
@@ -0,0 +1,71 @@
+# Orthanc - A Lightweight, RESTful DICOM Store
+# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+# Department, University Hospital of Liege, Belgium
+# Copyright (C) 2017-2023 Osimis S.A., Belgium
+# Copyright (C) 2021-2023 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+cmake_minimum_required(VERSION 2.8)
+
+project(MultitenantDicom)
+
+SET(ORTHANC_PLUGIN_VERSION "0.0" CACHE STRING "Version of the plugin")
+SET(STATIC_BUILD OFF CACHE BOOL "Static build of the third-party libraries (necessary for Windows)")
+SET(ALLOW_DOWNLOADS OFF CACHE BOOL "Allow CMake to download packages")
+
+include(${CMAKE_SOURCE_DIR}/../Common/OrthancPlugins.cmake)
+include(${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake)
+
+set(ENABLE_LOCALE ON CACHE INTERNAL "")
+set(ENABLE_DCMTK ON CACHE INTERNAL "")
+set(ENABLE_DCMTK_NETWORKING ON CACHE INTERNAL "")
+set(ENABLE_DCMTK_TRANSCODING OFF CACHE INTERNAL "")
+
+set(ENABLE_MODULE_DICOM ON CACHE INTERNAL "")
+set(ENABLE_MODULE_IMAGES ON CACHE INTERNAL "")
+set(ENABLE_MODULE_JOBS OFF CACHE INTERNAL "")
+
+include(${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake)
+
+add_library(MultitenantDicom SHARED 
+  DicomFilter.cpp
+  FindRequestHandler.cpp
+  MoveRequestHandler.cpp
+  MultitenantDicomServer.cpp
+  Plugin.cpp
+  PluginToolbox.cpp
+  StoreRequestHandler.cpp
+
+  ${CMAKE_SOURCE_DIR}/../Common/OrthancPluginCppWrapper.cpp
+  ${ORTHANC_CORE_SOURCES}
+  ${ORTHANC_DICOM_SOURCES}
+  ${AUTOGENERATED_SOURCES}
+  )
+
+target_link_libraries(MultitenantDicom ${DCMTK_LIBRARIES})
+
+message("Setting the version of the plugin to ${ORTHANC_PLUGIN_VERSION}")
+add_definitions(-DORTHANC_PLUGIN_VERSION="${ORTHANC_PLUGIN_VERSION}")
+
+set_target_properties(MultitenantDicom PROPERTIES 
+  VERSION ${ORTHANC_PLUGIN_VERSION} 
+  SOVERSION ${ORTHANC_PLUGIN_VERSION})
+
+install(
+  TARGETS MultitenantDicom
+  RUNTIME DESTINATION lib    # Destination for Windows
+  LIBRARY DESTINATION share/orthanc/plugins    # Destination for Linux
+  )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/DicomFilter.cpp	Fri Apr 14 11:49:24 2023 +0200
@@ -0,0 +1,209 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2021-2023 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "DicomFilter.h"
+
+#include "PluginToolbox.h"
+
+#include "../../../../OrthancFramework/Sources/Logging.h"
+#include "../../../../OrthancFramework/Sources/OrthancException.h"
+
+#include "../Common/OrthancPluginCppWrapper.h"
+
+
+DicomFilter::DicomFilter() :
+  hasAcceptedTransferSyntaxes_(false)
+{
+  {
+    OrthancPlugins::OrthancConfiguration config;
+    alwaysAllowEcho_ = config.GetBooleanValue("DicomAlwaysAllowEcho", true);
+    alwaysAllowFind_ = config.GetBooleanValue("DicomAlwaysAllowFind", false);
+    alwaysAllowMove_ = config.GetBooleanValue("DicomAlwaysAllowMove", false);
+    alwaysAllowStore_ = config.GetBooleanValue("DicomAlwaysAllowStore", true);
+    unknownSopClassAccepted_ = config.GetBooleanValue("UnknownSopClassAccepted", false);
+    isStrict_ = config.GetBooleanValue("StrictAetComparison", false);
+    checkModalityHost_ = config.GetBooleanValue("DicomCheckModalityHost", false);
+  }
+}
+
+
+bool DicomFilter::IsAllowedConnection(const std::string& remoteIp,
+                                      const std::string& remoteAet,
+                                      const std::string& calledAet)
+{
+  boost::shared_lock<boost::shared_mutex>  lock(mutex_);
+
+  LOG(INFO) << "Incoming connection from AET " << remoteAet
+            << " on IP " << remoteIp << ", calling AET " << calledAet;
+
+  if (alwaysAllowEcho_ ||
+      alwaysAllowFind_ ||
+      alwaysAllowMove_ ||
+      alwaysAllowStore_)
+  {
+    return true;
+  }
+  else
+  {
+    std::string name;
+    Orthanc::RemoteModalityParameters parameters;
+
+    if (!PluginToolbox::LookupAETitle(name, parameters, isStrict_, remoteAet))
+    {
+      LOG(WARNING) << "Modality \"" << remoteAet
+                   << "\" is not listed in the \"DicomModalities\" configuration option";
+      return false;
+    }
+    else if (!checkModalityHost_ ||
+             remoteIp == parameters.GetHost())
+    {
+      return true;
+    }
+    else
+    {
+      LOG(WARNING) << "Forbidding access from AET \"" << remoteAet
+                   << "\" given its hostname (" << remoteIp << ") does not match "
+                   << "the \"DicomModalities\" configuration option ("
+                   << parameters.GetHost() << " was expected)";
+      return false;
+    }
+  }
+}
+
+
+bool DicomFilter::IsAllowedRequest(const std::string& remoteIp,
+                                   const std::string& remoteAet,
+                                   const std::string& calledAet,
+                                   Orthanc::DicomRequestType type)
+{
+  boost::shared_lock<boost::shared_mutex>  lock(mutex_);
+
+  LOG(INFO) << "Incoming " << EnumerationToString(type) << " request from AET "
+            << remoteAet << " on IP " << remoteIp << ", calling AET " << calledAet;
+
+  if (type == Orthanc::DicomRequestType_Echo &&
+      alwaysAllowEcho_)
+  {
+    // Incoming C-Echo requests are always accepted, even from unknown AET
+    return true;
+  }
+  else if (type == Orthanc::DicomRequestType_Find &&
+           alwaysAllowFind_)
+  {
+    // Incoming C-Find requests are always accepted, even from unknown AET
+    return true;
+  }
+  else if (type == Orthanc::DicomRequestType_Store &&
+           alwaysAllowStore_)
+  {
+    // Incoming C-Store requests are always accepted, even from unknown AET
+    return true;
+  }
+  else if (type == Orthanc::DicomRequestType_Move &&
+           alwaysAllowMove_)
+  {
+    // Incoming C-Move requests are always accepted, even from unknown AET
+    return true;
+  }
+  else
+  {
+    std::string name;
+    Orthanc::RemoteModalityParameters parameters;
+
+    if (!PluginToolbox::LookupAETitle(name, parameters, isStrict_, remoteAet))
+    {
+      LOG(WARNING) << "DICOM authorization rejected for AET " << remoteAet
+                   << " on IP " << remoteIp << ": This AET is not listed in "
+                   << "configuration option \"DicomModalities\"";
+      return false;
+    }
+    else
+    {
+      if (parameters.IsRequestAllowed(type))
+      {
+        return true;
+      }
+      else
+      {
+        LOG(WARNING) << "DICOM authorization rejected for AET " << remoteAet
+                     << " on IP " << remoteIp << ": The DICOM command "
+                     << EnumerationToString(type) << " is not allowed for this modality "
+                     << "according to configuration option \"DicomModalities\"";
+        return false;
+      }
+    }
+  }
+}
+
+
+void DicomFilter::GetAcceptedTransferSyntaxes(std::set<Orthanc::DicomTransferSyntax>& target,
+                                              const std::string& remoteIp,
+                                              const std::string& remoteAet,
+                                              const std::string& calledAet)
+{
+  boost::unique_lock<boost::shared_mutex>  lock(mutex_);
+
+  if (!hasAcceptedTransferSyntaxes_)
+  {
+    Json::Value syntaxes;
+
+    if (!OrthancPlugins::RestApiGet(syntaxes, "/tools/accepted-transfer-syntaxes", false) ||
+        syntaxes.type() != Json::arrayValue)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+    else
+    {
+      for (Json::Value::ArrayIndex i = 0; i < syntaxes.size(); i++)
+      {
+        Orthanc::DicomTransferSyntax syntax;
+
+        if (syntaxes[i].type() != Json::stringValue)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+        else if (Orthanc::LookupTransferSyntax(syntax, syntaxes[i].asString()))
+        {
+          acceptedTransferSyntaxes_.insert(syntax);
+        }
+        else
+        {
+          LOG(WARNING) << "Unknown transfer syntax: " << syntaxes[i].asString();
+        }
+      }
+    }
+
+    hasAcceptedTransferSyntaxes_ = true;
+  }
+
+  target = acceptedTransferSyntaxes_;
+}
+
+
+bool DicomFilter::IsUnknownSopClassAccepted(const std::string& remoteIp,
+                                            const std::string& remoteAet,
+                                            const std::string& calledAet)
+{
+  boost::shared_lock<boost::shared_mutex>  lock(mutex_);
+  return unknownSopClassAccepted_;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/DicomFilter.h	Fri Apr 14 11:49:24 2023 +0200
@@ -0,0 +1,68 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2021-2023 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../../../OrthancFramework/Sources/Compatibility.h"
+#include "../../../../OrthancFramework/Sources/DicomNetworking/IApplicationEntityFilter.h"
+
+#include <boost/thread/shared_mutex.hpp>
+
+
+class DicomFilter : public Orthanc::IApplicationEntityFilter
+{
+private:
+  boost::shared_mutex mutex_;
+
+  bool alwaysAllowEcho_;
+  bool alwaysAllowFind_;
+  bool alwaysAllowMove_;
+  bool alwaysAllowStore_;
+  bool unknownSopClassAccepted_;
+  bool isStrict_;
+  bool checkModalityHost_;
+
+  bool hasAcceptedTransferSyntaxes_;
+  std::set<Orthanc::DicomTransferSyntax>  acceptedTransferSyntaxes_;
+
+public:
+  DicomFilter();
+
+  virtual bool IsAllowedConnection(const std::string& remoteIp,
+                                   const std::string& remoteAet,
+                                   const std::string& calledAet) ORTHANC_OVERRIDE;
+
+  virtual bool IsAllowedRequest(const std::string& remoteIp,
+                                const std::string& remoteAet,
+                                const std::string& calledAet,
+                                Orthanc::DicomRequestType type) ORTHANC_OVERRIDE;
+
+  virtual void GetAcceptedTransferSyntaxes(std::set<Orthanc::DicomTransferSyntax>& target,
+                                           const std::string& remoteIp,
+                                           const std::string& remoteAet,
+                                           const std::string& calledAet) ORTHANC_OVERRIDE;
+
+  virtual bool IsUnknownSopClassAccepted(const std::string& remoteIp,
+                                         const std::string& remoteAet,
+                                         const std::string& calledAet) ORTHANC_OVERRIDE;
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/FindRequestHandler.cpp	Fri Apr 14 11:49:24 2023 +0200
@@ -0,0 +1,124 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2021-2023 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "FindRequestHandler.h"
+
+#include "PluginToolbox.h"
+
+#include "../../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
+#include "../../../../OrthancFramework/Sources/OrthancException.h"
+
+#include "../Common/OrthancPluginCppWrapper.h"
+
+
+void FindRequestHandler::Handle(Orthanc::DicomFindAnswers& answers,
+                                const Orthanc::DicomMap& input,
+                                const std::list<Orthanc::DicomTag>& sequencesToReturn,
+                                const std::string& remoteIp,
+                                const std::string& remoteAet,
+                                const std::string& calledAet,
+                                Orthanc::ModalityManufacturer manufacturer)
+{
+  std::set<Orthanc::DicomTag> tags;
+  input.GetTags(tags);
+
+  Json::Value request = Json::objectValue;
+  request["Expand"] = true;
+  PluginToolbox::AddLabelsToFindRequest(request, labels_, constraint_);
+
+  Json::Value query = Json::objectValue;
+  std::string level;
+
+  for (std::set<Orthanc::DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it)
+  {
+    std::string s;
+
+    if (input.LookupStringValue(s, *it, false) &&
+        !s.empty())
+    {
+      if (*it == Orthanc::DICOM_TAG_QUERY_RETRIEVE_LEVEL)
+      {
+        level = s;
+      }
+      else 
+      {
+        query[it->Format()] = s;
+      }
+    }
+  }
+
+  if (level.empty())
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol, "Missing QueryRetrieveLevel in DICOM C-FIND request");
+  }
+
+  request["Level"] = EnumerationToString(PluginToolbox::ParseQueryRetrieveLevel(level));
+  request["Query"] = query;
+
+  Json::Value response;
+  if (!OrthancPlugins::RestApiPost(response, "/tools/find", request, false) ||
+      response.type() != Json::arrayValue)
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol, "Invalid DICOM C-FIND request");
+  }
+
+  for (Json::Value::ArrayIndex i = 0; i < response.size(); i++)
+  {
+    if (response[i].type() != Json::objectValue ||
+        !response[i].isMember(KEY_MAIN_DICOM_TAGS))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+
+    if (response[i].isMember(KEY_PATIENT_MAIN_DICOM_TAGS) &&
+        response[i][KEY_PATIENT_MAIN_DICOM_TAGS].type() != Json::objectValue)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+            
+    Orthanc::DicomMap m;
+
+    for (std::set<Orthanc::DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it)
+    {
+      const std::string tag = Orthanc::FromDcmtkBridge::GetTagName(*it, "");
+
+      if (response[i][KEY_MAIN_DICOM_TAGS].isMember(tag) &&
+          response[i][KEY_MAIN_DICOM_TAGS][tag].type() == Json::stringValue)
+      {
+        m.SetValue(*it, response[i][KEY_MAIN_DICOM_TAGS][tag].asString(), false);
+      }
+      else if (response[i].isMember(KEY_PATIENT_MAIN_DICOM_TAGS) &&
+               response[i][KEY_PATIENT_MAIN_DICOM_TAGS].isMember(tag) &&
+               response[i][KEY_PATIENT_MAIN_DICOM_TAGS][tag].type() == Json::stringValue)
+      {
+        m.SetValue(*it, response[i][KEY_PATIENT_MAIN_DICOM_TAGS][tag].asString(), false);
+      }        
+    }
+            
+    m.SetValue(Orthanc::DICOM_TAG_QUERY_RETRIEVE_LEVEL, level, false);
+    m.SetValue(Orthanc::DICOM_TAG_RETRIEVE_AE_TITLE, retrieveAet_, false);
+    answers.Add(m);
+  }
+
+  answers.SetComplete(true);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/FindRequestHandler.h	Fri Apr 14 11:49:24 2023 +0200
@@ -0,0 +1,56 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2021-2023 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "PluginEnumerations.h"
+
+#include "../../../../OrthancFramework/Sources/DicomNetworking/IFindRequestHandler.h"
+
+
+class FindRequestHandler : public Orthanc::IFindRequestHandler
+{
+private:
+  // Everything is constant, so no need for a mutex
+  const std::string           retrieveAet_;
+  const std::set<std::string> labels_;
+  const LabelsConstraint      constraint_;
+        
+public:
+  FindRequestHandler(const std::string& retrieveAet,
+                     const std::set<std::string>& labels,
+                     LabelsConstraint constraint) :
+    retrieveAet_(retrieveAet),
+    labels_(labels),
+    constraint_(constraint)
+  {
+  }
+
+  virtual void Handle(Orthanc::DicomFindAnswers& answers,
+                      const Orthanc::DicomMap& input,
+                      const std::list<Orthanc::DicomTag>& sequencesToReturn,
+                      const std::string& remoteIp,
+                      const std::string& remoteAet,
+                      const std::string& calledAet,
+                      Orthanc::ModalityManufacturer manufacturer) ORTHANC_OVERRIDE;
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/MoveRequestHandler.cpp	Fri Apr 14 11:49:24 2023 +0200
@@ -0,0 +1,231 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2021-2023 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "MoveRequestHandler.h"
+
+#include "PluginToolbox.h"
+
+#include "../../../../OrthancFramework/Sources/OrthancException.h"
+#include "../../../../OrthancFramework/Sources/Toolbox.h"
+
+#include "../Common/OrthancPluginCppWrapper.h"
+
+
+class MoveRequestHandler::Iterator : public Orthanc::IMoveRequestIterator
+{
+private:
+  std::string  targetModality_;
+  Json::Value  body_;
+  bool         done_;
+
+public:
+  Iterator(const std::string& targetModality,
+           const Json::Value& body) :
+    targetModality_(targetModality),
+    body_(body),
+    done_(false)
+  {
+  }
+
+  unsigned int GetSubOperationCount() const
+  {
+    return 1;
+  }
+
+  Status DoNext()
+  {
+    Json::Value answer;
+
+    if (done_)
+    {
+      return Status_Failure;
+    }
+    else if (OrthancPlugins::RestApiPost(answer, "/modalities/" + targetModality_ + "/store", body_, false))
+    {
+      done_ = true;
+      return Status_Success;
+    }
+    else
+    {
+      done_ = true;
+      return Status_Failure;
+    }
+  }
+};
+
+
+void MoveRequestHandler::ExecuteLookup(std::set<std::string>& publicIds,
+                                       Orthanc::ResourceType level,
+                                       const Orthanc::DicomTag& tag,
+                                       const std::string& value) const
+{
+  std::vector<std::string> tokens;
+  Orthanc::Toolbox::TokenizeString(tokens, value, '\\');
+
+  for (size_t i = 0; i < tokens.size(); i++)
+  {
+    if (!tokens[i].empty())
+    {
+      Json::Value request = Json::objectValue;
+      request["Level"] = Orthanc::EnumerationToString(level);
+      request["Query"][tag.Format()] = tokens[i];
+      PluginToolbox::AddLabelsToFindRequest(request, labels_, constraint_);
+
+      Json::Value response;
+      if (OrthancPlugins::RestApiPost(response, "/tools/find", request, false) &&
+          response.type() == Json::arrayValue)
+      {
+        for (Json::Value::ArrayIndex i = 0; i < response.size(); i++)
+        {
+          if (response[i].type() != Json::stringValue)
+          {
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+          }
+          else
+          {
+            publicIds.insert(response[i].asString());
+          }
+        }
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+    }
+  }
+}
+
+
+void MoveRequestHandler::LookupIdentifiers(std::set<std::string>& publicIds,
+                                           Orthanc::ResourceType level,
+                                           const Orthanc::DicomMap& input) const
+{
+  std::string value;
+
+  switch (level)
+  {
+    case Orthanc::ResourceType_Patient:
+      if (input.LookupStringValue(value, Orthanc::DICOM_TAG_PATIENT_ID, false) &&
+          !value.empty())
+      {
+        ExecuteLookup(publicIds, level, Orthanc::DICOM_TAG_PATIENT_ID, value);
+      }
+      break;
+
+    case Orthanc::ResourceType_Study:
+      if (input.LookupStringValue(value, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, false) &&
+          !value.empty())
+      {
+        ExecuteLookup(publicIds, level, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, value);
+      }
+      else if (input.LookupStringValue(value, Orthanc::DICOM_TAG_ACCESSION_NUMBER, false) &&
+               !value.empty())
+      {
+        ExecuteLookup(publicIds, level, Orthanc::DICOM_TAG_ACCESSION_NUMBER, value);
+      }
+      break;
+
+    case Orthanc::ResourceType_Series:
+      if (input.LookupStringValue(value, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, false) &&
+          !value.empty())
+      {
+        ExecuteLookup(publicIds, level, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, value);
+      }
+      break;
+
+    case Orthanc::ResourceType_Instance:
+      if (input.LookupStringValue(value, Orthanc::DICOM_TAG_SOP_INSTANCE_UID, false) &&
+          !value.empty())
+      {
+        ExecuteLookup(publicIds, level, Orthanc::DICOM_TAG_SOP_INSTANCE_UID, value);
+      }
+      break;
+
+    default:
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+  }
+}
+
+
+Orthanc::IMoveRequestIterator* MoveRequestHandler::Handle(const std::string& targetAet,
+                                                          const Orthanc::DicomMap& input,
+                                                          const std::string& originatorIp,
+                                                          const std::string& originatorAet,
+                                                          const std::string& calledAet,
+                                                          uint16_t originatorId)
+{
+  std::set<std::string> publicIds;
+
+  std::string s;
+  if (input.LookupStringValue(s, Orthanc::DICOM_TAG_QUERY_RETRIEVE_LEVEL, false) &&
+      !s.empty())
+  {
+    LookupIdentifiers(publicIds, PluginToolbox::ParseQueryRetrieveLevel(s), input);
+  }
+  else
+  {
+    // The query level is not present in the C-Move request, which
+    // does not follow the DICOM standard. This is for instance the
+    // behavior of Tudor DICOM. Try and automatically deduce the
+    // query level: Start from the instance level, going up to the
+    // patient level until a valid DICOM identifier is found.
+    LookupIdentifiers(publicIds, Orthanc::ResourceType_Instance, input);
+
+    if (publicIds.empty())
+    {
+      LookupIdentifiers(publicIds, Orthanc::ResourceType_Series, input);
+    }
+
+    if (publicIds.empty())
+    {
+      LookupIdentifiers(publicIds, Orthanc::ResourceType_Study, input);
+    }
+
+    if (publicIds.empty())
+    {
+      LookupIdentifiers(publicIds, Orthanc::ResourceType_Patient, input);
+    }
+  }
+
+  Json::Value resources = Json::arrayValue;
+  for (std::set<std::string>::const_iterator it = publicIds.begin(); it != publicIds.end(); ++it)
+  {
+    resources.append(*it);
+  }
+
+  std::string targetName;
+  Orthanc::RemoteModalityParameters targetParameters;
+  if (!PluginToolbox::LookupAETitle(targetName, targetParameters, isStrictAet_, targetAet))
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol, "Unknown target AET: " + targetAet);
+  }
+
+  Json::Value body;
+  body["CalledAet"] = calledAet;
+  body["MoveOriginatorAet"] = originatorAet;
+  body["MoveOriginatorID"] = originatorId;
+  body["Resources"] = resources;
+  body["Synchronous"] = isSynchronous_;
+
+  return new Iterator(targetName, body);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/MoveRequestHandler.h	Fri Apr 14 11:49:24 2023 +0200
@@ -0,0 +1,69 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2021-2023 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "PluginEnumerations.h"
+
+#include "../../../../OrthancFramework/Sources/DicomNetworking/IMoveRequestHandler.h"
+
+
+class MoveRequestHandler : public Orthanc::IMoveRequestHandler
+{
+private:
+  class Iterator;
+
+  // Everything is constant, so no need for a mutex
+  const std::set<std::string>  labels_;
+  const LabelsConstraint       constraint_;
+  const bool                   isStrictAet_;
+  const bool                   isSynchronous_;
+
+  void ExecuteLookup(std::set<std::string>& publicIds,
+                     Orthanc::ResourceType level,
+                     const Orthanc::DicomTag& tag,
+                     const std::string& value) const;
+
+  void LookupIdentifiers(std::set<std::string>& publicIds,
+                         Orthanc::ResourceType level,
+                         const Orthanc::DicomMap& input) const;
+  
+public:
+  MoveRequestHandler(const std::set<std::string>& labels,
+                     LabelsConstraint constraint,
+                     bool isStrictAet,
+                     bool isSynchronous) :
+    labels_(labels),
+    constraint_(constraint),
+    isStrictAet_(isStrictAet),
+    isSynchronous_(isSynchronous)
+  {
+  }
+
+  virtual Orthanc::IMoveRequestIterator* Handle(const std::string& targetAet,
+                                                const Orthanc::DicomMap& input,
+                                                const std::string& originatorIp,
+                                                const std::string& originatorAet,
+                                                const std::string& calledAet,
+                                                uint16_t originatorId) ORTHANC_OVERRIDE;
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/MultitenantDicomServer.cpp	Fri Apr 14 11:49:24 2023 +0200
@@ -0,0 +1,146 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2021-2023 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "MultitenantDicomServer.h"
+
+#include "FindRequestHandler.h"
+#include "MoveRequestHandler.h"
+#include "PluginToolbox.h"
+#include "StoreRequestHandler.h"
+
+#include "../../../../OrthancFramework/Sources/Logging.h"
+#include "../../../../OrthancFramework/Sources/SerializationToolbox.h"
+
+#include "../Common/OrthancPluginCppWrapper.h"
+
+
+bool MultitenantDicomServer::IsSameAETitle(const std::string& aet1,
+                                           const std::string& aet2)
+{
+  boost::mutex::scoped_lock lock(mutex_);
+  return PluginToolbox::IsSameAETitle(isStrictAet_, aet1, aet2);
+}
+
+
+bool MultitenantDicomServer::LookupAETitle(Orthanc::RemoteModalityParameters& parameters,
+                                           const std::string& aet)
+{
+  boost::mutex::scoped_lock lock(mutex_);
+
+  std::string name;
+  return PluginToolbox::LookupAETitle(name, parameters, isStrictAet_, server_->GetApplicationEntityTitle());
+}
+
+
+Orthanc::IFindRequestHandler* MultitenantDicomServer::ConstructFindRequestHandler()
+{
+  boost::mutex::scoped_lock lock(mutex_);
+  return new FindRequestHandler(server_->GetApplicationEntityTitle(), labels_, labelsConstraint_);
+}
+
+
+Orthanc::IMoveRequestHandler* MultitenantDicomServer::ConstructMoveRequestHandler()
+{
+  boost::mutex::scoped_lock lock(mutex_);
+  return new MoveRequestHandler(labels_, labelsConstraint_, isStrictAet_, isSynchronousCMove_);
+}
+
+
+Orthanc::IStoreRequestHandler* MultitenantDicomServer::ConstructStoreRequestHandler()
+{
+  boost::mutex::scoped_lock lock(mutex_);
+  return new StoreRequestHandler(labels_, labelsStoreLevels_);
+}
+
+
+MultitenantDicomServer::MultitenantDicomServer(const Json::Value& serverConfig)
+{
+  PluginToolbox::ParseLabels(labels_, labelsConstraint_, serverConfig);
+
+  if (serverConfig.isMember(KEY_LABELS_STORE_LEVELS))
+  {
+    std::set<std::string> levels;
+    Orthanc::SerializationToolbox::ReadSetOfStrings(levels, serverConfig, KEY_LABELS_STORE_LEVELS);
+    for (std::set<std::string>::const_iterator it = levels.begin(); it != levels.end(); ++it)
+    {
+      labelsStoreLevels_.insert(Orthanc::StringToResourceType(it->c_str()));
+    }
+  }
+  else
+  {
+    labelsStoreLevels_.insert(Orthanc::ResourceType_Study);
+    labelsStoreLevels_.insert(Orthanc::ResourceType_Series);
+    labelsStoreLevels_.insert(Orthanc::ResourceType_Instance);
+  }
+  
+  server_.reset(new Orthanc::DicomServer);
+
+  {
+    OrthancPlugins::OrthancConfiguration globalConfig;
+    isSynchronousCMove_ = globalConfig.GetBooleanValue(KEY_SYNCHRONOUS_C_MOVE, true);
+    isStrictAet_ = globalConfig.GetBooleanValue(KEY_STRICT_AET_COMPARISON, false);
+
+    server_->SetCalledApplicationEntityTitleCheck(globalConfig.GetBooleanValue("DicomCheckCalledAet", false));
+    server_->SetAssociationTimeout(globalConfig.GetUnsignedIntegerValue("DicomScpTimeout", 30));
+    server_->SetThreadsCount(globalConfig.GetUnsignedIntegerValue("DicomThreadsCount", 1));
+    server_->SetMaximumPduLength(globalConfig.GetUnsignedIntegerValue("MaximumPduLength", 16384));
+  }
+
+  server_->SetRemoteModalities(*this);
+  server_->SetApplicationEntityFilter(filter_);
+  server_->SetPortNumber(Orthanc::SerializationToolbox::ReadUnsignedInteger(serverConfig, "Port"));
+  server_->SetApplicationEntityTitle(Orthanc::SerializationToolbox::ReadString(serverConfig, KEY_AET));
+  server_->SetFindRequestHandlerFactory(*this);
+  server_->SetMoveRequestHandlerFactory(*this);
+  server_->SetStoreRequestHandlerFactory(*this);
+}
+
+
+void MultitenantDicomServer::Start()
+{
+  boost::mutex::scoped_lock lock(mutex_);
+
+  if (server_->GetPortNumber() < 1024)
+  {
+    LOG(WARNING) << "The DICOM port is privileged ("
+                 << server_->GetPortNumber() << " is below 1024), "
+                 << "make sure you run Orthanc as root/administrator";
+  }
+
+  server_->Start();
+  LOG(WARNING) << "Started multitenant DICOM server listening with AET " << server_->GetApplicationEntityTitle()
+               << " on port: " << server_->GetPortNumber();
+}
+
+
+void MultitenantDicomServer::Stop()
+{
+  boost::mutex::scoped_lock lock(mutex_);
+
+  if (server_.get() != NULL)
+  {
+    LOG(WARNING) << "Stopping multitenant DICOM server listening with AET " << server_->GetApplicationEntityTitle()
+                 << " on port: " << server_->GetPortNumber();
+    server_->Stop();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/MultitenantDicomServer.h	Fri Apr 14 11:49:24 2023 +0200
@@ -0,0 +1,69 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2021-2023 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "DicomFilter.h"
+#include "PluginEnumerations.h"
+
+#include "../../../../OrthancFramework/Sources/DicomNetworking/DicomServer.h"
+
+#include <boost/thread/mutex.hpp>
+
+
+class MultitenantDicomServer :
+  private Orthanc::DicomServer::IRemoteModalities,
+  private Orthanc::IFindRequestHandlerFactory,
+  private Orthanc::IMoveRequestHandlerFactory,
+  private Orthanc::IStoreRequestHandlerFactory
+{
+private:
+  virtual bool IsSameAETitle(const std::string& aet1,
+                             const std::string& aet2) ORTHANC_OVERRIDE;
+
+  virtual bool LookupAETitle(Orthanc::RemoteModalityParameters& parameters,
+                             const std::string& aet) ORTHANC_OVERRIDE;
+
+  virtual Orthanc::IFindRequestHandler* ConstructFindRequestHandler() ORTHANC_OVERRIDE;
+
+  virtual Orthanc::IMoveRequestHandler* ConstructMoveRequestHandler() ORTHANC_OVERRIDE;
+
+  virtual Orthanc::IStoreRequestHandler* ConstructStoreRequestHandler() ORTHANC_OVERRIDE;
+
+  boost::mutex  mutex_;
+
+  std::set<std::string>                  labels_;
+  LabelsConstraint                       labelsConstraint_;
+  std::set<Orthanc::ResourceType>        labelsStoreLevels_;
+  bool                                   isSynchronousCMove_;
+  bool                                   isStrictAet_;
+  DicomFilter                            filter_;
+  std::unique_ptr<Orthanc::DicomServer>  server_;
+
+public:
+  MultitenantDicomServer(const Json::Value& serverConfig);
+
+  void Start();
+
+  void Stop();
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/NOTES.txt	Fri Apr 14 11:49:24 2023 +0200
@@ -0,0 +1,8 @@
+
+Linux Standard Base (LSB)
+=========================
+
+$ mkdir lsb
+$ cd lsb
+$ LSB_CC=gcc-4.8 LSB_CXX=g++-4.8 cmake .. -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE=../../../../OrthancFramework/Resources/Toolchains/LinuxStandardBaseToolchain.cmake -DUSE_LEGACY_JSONCPP=ON -DUSE_LEGACY_BOOST=ON -G Ninja
+$ ninja
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/OrthancFrameworkDependencies.cpp	Fri Apr 14 11:49:24 2023 +0200
@@ -0,0 +1,85 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2021-2023 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+/**
+ * Remove the dependency upon ICU in plugins, as this greatly increase
+ * the size of the resulting binaries, since they must embed the ICU
+ * dictionary.
+ **/
+
+#define ORTHANC_ENABLE_ICU 0
+#define ORTHANC_FRAMEWORK_INCLUDE_RESOURCES 0
+
+#include <MultitenantDicomResources.h>
+
+#include "../../../../OrthancFramework/Sources/ChunkedBuffer.cpp"
+#include "../../../../OrthancFramework/Sources/Compression/DeflateBaseCompressor.cpp"
+#include "../../../../OrthancFramework/Sources/Compression/GzipCompressor.cpp"
+#include "../../../../OrthancFramework/Sources/Compression/ZlibCompressor.cpp"
+#include "../../../../OrthancFramework/Sources/DicomFormat/DicomArray.cpp"
+#include "../../../../OrthancFramework/Sources/DicomFormat/DicomElement.cpp"
+#include "../../../../OrthancFramework/Sources/DicomFormat/DicomImageInformation.cpp"
+#include "../../../../OrthancFramework/Sources/DicomFormat/DicomInstanceHasher.cpp"
+#include "../../../../OrthancFramework/Sources/DicomFormat/DicomIntegerPixelAccessor.cpp"
+#include "../../../../OrthancFramework/Sources/DicomFormat/DicomMap.cpp"
+#include "../../../../OrthancFramework/Sources/DicomFormat/DicomPath.cpp"
+#include "../../../../OrthancFramework/Sources/DicomFormat/DicomTag.cpp"
+#include "../../../../OrthancFramework/Sources/DicomFormat/DicomValue.cpp"
+#include "../../../../OrthancFramework/Sources/DicomNetworking/DicomAssociation.cpp"
+#include "../../../../OrthancFramework/Sources/DicomNetworking/DicomAssociationParameters.cpp"
+#include "../../../../OrthancFramework/Sources/DicomNetworking/DicomFindAnswers.cpp"
+#include "../../../../OrthancFramework/Sources/DicomNetworking/DicomServer.cpp"
+#include "../../../../OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp"
+#include "../../../../OrthancFramework/Sources/DicomNetworking/Internals/DicomTls.cpp"
+#include "../../../../OrthancFramework/Sources/DicomNetworking/Internals/FindScp.cpp"
+#include "../../../../OrthancFramework/Sources/DicomNetworking/Internals/GetScp.cpp"
+#include "../../../../OrthancFramework/Sources/DicomNetworking/Internals/MoveScp.cpp"
+#include "../../../../OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.cpp"
+#include "../../../../OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.cpp"
+#include "../../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp"
+#include "../../../../OrthancFramework/Sources/DicomParsing/Internals/DicomFrameIndex.cpp"
+#include "../../../../OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.cpp"
+#include "../../../../OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp"
+#include "../../../../OrthancFramework/Sources/DicomParsing/ToDcmtkBridge.cpp"
+#include "../../../../OrthancFramework/Sources/Enumerations.cpp"
+#include "../../../../OrthancFramework/Sources/HttpServer/HttpOutput.cpp"
+#include "../../../../OrthancFramework/Sources/Images/IImageWriter.cpp"
+#include "../../../../OrthancFramework/Sources/Images/Image.cpp"
+#include "../../../../OrthancFramework/Sources/Images/ImageAccessor.cpp"
+#include "../../../../OrthancFramework/Sources/Images/ImageBuffer.cpp"
+#include "../../../../OrthancFramework/Sources/Images/ImageProcessing.cpp"
+#include "../../../../OrthancFramework/Sources/Images/JpegErrorManager.cpp"
+#include "../../../../OrthancFramework/Sources/Images/JpegReader.cpp"
+#include "../../../../OrthancFramework/Sources/Images/JpegWriter.cpp"
+#include "../../../../OrthancFramework/Sources/Images/PamReader.cpp"
+#include "../../../../OrthancFramework/Sources/Images/PamWriter.cpp"
+#include "../../../../OrthancFramework/Sources/Images/PngReader.cpp"
+#include "../../../../OrthancFramework/Sources/Images/PngWriter.cpp"
+#include "../../../../OrthancFramework/Sources/Logging.cpp"
+#include "../../../../OrthancFramework/Sources/MultiThreading/RunnableWorkersPool.cpp"
+#include "../../../../OrthancFramework/Sources/MultiThreading/SharedMessageQueue.cpp"
+#include "../../../../OrthancFramework/Sources/OrthancException.cpp"
+#include "../../../../OrthancFramework/Sources/RestApi/RestApiOutput.cpp"
+#include "../../../../OrthancFramework/Sources/SerializationToolbox.cpp"
+#include "../../../../OrthancFramework/Sources/SystemToolbox.cpp"
+#include "../../../../OrthancFramework/Sources/Toolbox.cpp"
+#include "../../../../OrthancFramework/Sources/TemporaryFile.cpp"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/Plugin.cpp	Fri Apr 14 11:49:24 2023 +0200
@@ -0,0 +1,195 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2021-2023 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "MultitenantDicomServer.h"
+
+#include "../../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
+#include "../../../../OrthancFramework/Sources/Logging.h"
+#include "../../../../OrthancFramework/Sources/OrthancException.h"
+
+#include "../Common/OrthancPluginCppWrapper.h"
+
+
+typedef std::list<MultitenantDicomServer*> DicomServers;
+
+static DicomServers dicomServers_;
+
+
+static OrthancPluginErrorCode OnChangeCallback(OrthancPluginChangeType changeType,
+                                               OrthancPluginResourceType resourceType,
+                                               const char* resourceId)
+{
+  switch (changeType)
+  {
+    case OrthancPluginChangeType_OrthancStarted:
+    {
+      for (DicomServers::iterator it = dicomServers_.begin(); it != dicomServers_.end(); ++it)
+      {    
+        if (*it != NULL)
+        {
+          try
+          {
+            (*it)->Start();
+          }
+          catch (Orthanc::OrthancException& e)
+          {
+            LOG(ERROR) << "Exception while stopping the multitenant DICOM server: " << e.What();
+          }
+        }
+      }
+
+      break;
+    }
+    
+    case OrthancPluginChangeType_OrthancStopped:
+    {
+      for (DicomServers::iterator it = dicomServers_.begin(); it != dicomServers_.end(); ++it)
+      {    
+        if (*it != NULL)
+        {
+          try
+          {
+            (*it)->Stop();
+          }
+          catch (Orthanc::OrthancException& e)
+          {
+            LOG(ERROR) << "Exception while stopping the multitenant DICOM server: " << e.What();
+          }
+        }
+      }
+      
+      break;
+    }
+    
+    default:
+      break;
+  }
+
+  return OrthancPluginErrorCode_Success;
+}
+
+
+extern "C"
+{
+  ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context)
+  {
+    OrthancPlugins::SetGlobalContext(context);
+
+    /* Check the version of the Orthanc core */
+    if (OrthancPluginCheckVersion(OrthancPlugins::GetGlobalContext()) == 0)
+    {
+      char info[1024];
+      sprintf(info, "Your version of Orthanc (%s) must be above %d.%d.%d to run this plugin",
+              OrthancPlugins::GetGlobalContext()->orthancVersion,
+              ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
+              ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER,
+              ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER);
+      OrthancPluginLogError(OrthancPlugins::GetGlobalContext(), info);
+      return -1;
+    }
+
+#if ORTHANC_FRAMEWORK_VERSION_IS_ABOVE(1, 7, 2)
+    Orthanc::Logging::InitializePluginContext(context);
+#else
+    Orthanc::Logging::Initialize(context);
+#endif
+
+    if (!OrthancPlugins::CheckMinimalOrthancVersion(1, 12, 0))
+    {
+      OrthancPlugins::ReportMinimalOrthancVersion(1, 12, 0);
+      return -1;
+    }
+
+    Orthanc::FromDcmtkBridge::InitializeDictionary(false /* loadPrivateDictionary */);
+    /* Disable "gethostbyaddr" (which results in memory leaks) and use raw IP addresses */
+    dcmDisableGethostbyaddr.set(OFTrue);
+    
+    OrthancPluginSetDescription(context, "Multitenant plugin for Orthanc.");
+
+    OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback);
+    
+    try
+    {
+      OrthancPlugins::OrthancConfiguration globalConfig;
+
+      OrthancPlugins::OrthancConfiguration pluginConfig;
+      globalConfig.GetSection(pluginConfig, KEY_MULTITENANT_DICOM);
+
+      if (pluginConfig.GetJson().isMember(KEY_SERVERS))
+      {
+        const Json::Value& servers = pluginConfig.GetJson() [KEY_SERVERS];
+
+        if (servers.type() != Json::arrayValue)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadParameterType, "Configuration option \"" +
+                                          std::string(KEY_MULTITENANT_DICOM) + "." + std::string(KEY_SERVERS) + "\" must be an array");
+        }
+        else
+        {
+          for (Json::Value::ArrayIndex i = 0; i < servers.size(); i++)
+          {
+            dicomServers_.push_back(new MultitenantDicomServer(servers[i]));
+          }
+        }
+      }
+      
+      return 0;
+    }
+    catch (Orthanc::OrthancException& e)
+    {
+      LOG(ERROR) << "Exception while starting the multitenant DICOM server: " << e.What();
+      return -1;
+    }
+  }
+
+
+  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
+  {
+    for (DicomServers::iterator it = dicomServers_.begin(); it != dicomServers_.end(); ++it)
+    {    
+      if (*it != NULL)
+      {
+        try
+        {
+          delete *it;
+        }
+        catch (Orthanc::OrthancException& e)
+        {
+          LOG(ERROR) << "Exception while destroying the multitenant DICOM server: " << e.What();
+        }
+      }
+    }
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
+  {
+    return "multitenant-dicom";
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
+  {
+    return ORTHANC_PLUGIN_VERSION;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/PluginEnumerations.h	Fri Apr 14 11:49:24 2023 +0200
@@ -0,0 +1,48 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2021-2023 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+
+enum LabelsConstraint
+{
+  LabelsConstraint_All,
+  LabelsConstraint_Any,
+  LabelsConstraint_None
+};
+
+
+#define KEY_AET                      "AET"
+#define KEY_ALL                      "All"
+#define KEY_ANY                      "Any"
+#define KEY_LABELS                   "Labels"
+#define KEY_LABELS_CONSTRAINT        "LabelsConstraint"
+#define KEY_LABELS_STORE_LEVELS      "LabelsStoreLevels"
+#define KEY_MAIN_DICOM_TAGS          "MainDicomTags"
+#define KEY_MULTITENANT_DICOM        "MultitenantDicom"
+#define KEY_NONE                     "None"
+#define KEY_PATIENT_MAIN_DICOM_TAGS  "PatientMainDicomTags"
+#define KEY_QUERY                    "Query"
+#define KEY_SERVERS                  "Servers"
+#define KEY_STRICT_AET_COMPARISON    "StrictAetComparison"
+#define KEY_SYNCHRONOUS_C_MOVE       "SynchronousCMove"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/PluginToolbox.cpp	Fri Apr 14 11:49:24 2023 +0200
@@ -0,0 +1,207 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2021-2023 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "PluginToolbox.h"
+
+#include "../../../../OrthancFramework/Sources/OrthancException.h"
+#include "../../../../OrthancFramework/Sources/SerializationToolbox.h"
+#include "../../../../OrthancFramework/Sources/Toolbox.h"
+
+#include "../Common/OrthancPluginCppWrapper.h"
+
+
+namespace PluginToolbox
+{
+  bool IsValidLabel(const std::string& label)
+  {
+    if (label.empty())
+    {
+      return false;
+    }
+
+    if (label.size() > 64)
+    {
+      // This limitation is for MySQL, which cannot use a TEXT
+      // column of undefined length as a primary key
+      return false;
+    }
+      
+    for (size_t i = 0; i < label.size(); i++)
+    {
+      if (!(label[i] == '_' ||
+            label[i] == '-' ||
+            (label[i] >= 'a' && label[i] <= 'z') ||
+            (label[i] >= 'A' && label[i] <= 'Z') ||
+            (label[i] >= '0' && label[i] <= '9')))
+      {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+
+  Orthanc::ResourceType ParseQueryRetrieveLevel(const std::string& level)
+  {
+    if (level == "PATIENT")
+    {
+      return Orthanc::ResourceType_Patient;
+    }
+    else if (level == "STUDY")
+    {
+      return Orthanc::ResourceType_Study;
+    }
+    else if (level == "SERIES")
+    {
+      return Orthanc::ResourceType_Series;
+    }
+    else if (level == "INSTANCE")
+    {
+      return Orthanc::ResourceType_Instance;
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol, "Bad value for QueryRetrieveLevel in DICOM C-FIND: " + level);
+    }
+  }
+
+
+  bool IsSameAETitle(bool isStrict,
+                     const std::string& aet1,
+                     const std::string& aet2)
+  {
+    if (isStrict)
+    {
+      // Case-sensitive matching
+      return aet1 == aet2;
+    }
+    else
+    {
+      // Case-insensitive matching (default)
+      std::string tmp1, tmp2;
+      Orthanc::Toolbox::ToLowerCase(tmp1, aet1);
+      Orthanc::Toolbox::ToLowerCase(tmp2, aet2);
+      return tmp1 == tmp2;
+    }
+  }
+
+
+  bool LookupAETitle(std::string& name,
+                     Orthanc::RemoteModalityParameters& parameters,
+                     bool isStrict,
+                     const std::string& aet)
+  {
+    Json::Value modalities;
+    if (!OrthancPlugins::RestApiGet(modalities, "/modalities?expand", false))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, "Unable to obtain the list of the remote modalities");
+    }
+
+    std::vector<std::string> names = modalities.getMemberNames();
+    for (size_t i = 0; i < names.size(); i++)
+    {
+      parameters = Orthanc::RemoteModalityParameters(modalities[names[i]]);
+      
+      if (IsSameAETitle(isStrict, parameters.GetApplicationEntityTitle(), aet))
+      {
+        name = names[i];
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+
+  void ParseLabels(std::set<std::string>& targetLabels,
+                   LabelsConstraint& targetConstraint,
+                   const Json::Value& serverConfig)
+  {
+    Orthanc::SerializationToolbox::ReadSetOfStrings(targetLabels, serverConfig, KEY_LABELS);
+
+    for (std::set<std::string>::const_iterator it = targetLabels.begin(); it != targetLabels.end(); ++it)
+    {
+      if (!PluginToolbox::IsValidLabel(*it))
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, "Invalid label: " + *it);
+      }
+    }
+
+    std::string s = Orthanc::SerializationToolbox::ReadString(serverConfig, KEY_LABELS_CONSTRAINT, KEY_ALL);
+    targetConstraint = PluginToolbox::StringToLabelsConstraint(s);
+  }
+  
+
+  void AddLabelsToFindRequest(Json::Value& request,
+                              const std::set<std::string>& labels,
+                              LabelsConstraint constraint)
+  {
+    Json::Value items = Json::arrayValue;
+    for (std::set<std::string>::const_iterator it = labels.begin(); it != labels.end(); ++it)
+    {
+      items.append(*it);
+    }
+
+    request[KEY_LABELS] = items;
+
+    switch (constraint)
+    {
+      case LabelsConstraint_All:
+        request[KEY_LABELS_CONSTRAINT] = KEY_ALL;
+        break;
+
+      case LabelsConstraint_Any:
+        request[KEY_LABELS_CONSTRAINT] = KEY_ANY;
+        break;
+
+      case LabelsConstraint_None:
+        request[KEY_LABELS_CONSTRAINT] = KEY_NONE;
+        break;
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  LabelsConstraint StringToLabelsConstraint(const std::string& s)
+  {
+    if (s == KEY_ALL)
+    {
+      return LabelsConstraint_All;
+    }
+    else if (s == KEY_ANY)
+    {
+      return LabelsConstraint_Any;
+    }
+    else if (s == KEY_NONE)
+    {
+      return LabelsConstraint_None;
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, "Bad value for constraint of labels: " + s);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/PluginToolbox.h	Fri Apr 14 11:49:24 2023 +0200
@@ -0,0 +1,56 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2021-2023 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "PluginEnumerations.h"
+
+#include "../../../../OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.h"
+
+#include <json/value.h>
+
+namespace PluginToolbox
+{
+  bool IsValidLabel(const std::string& label);
+  
+  Orthanc::ResourceType ParseQueryRetrieveLevel(const std::string& level);
+  
+  bool IsSameAETitle(bool isStrict,
+                     const std::string& aet1,
+                     const std::string& aet2);
+
+  bool LookupAETitle(std::string& name,
+                     Orthanc::RemoteModalityParameters& parameters,
+                     bool isStrict,
+                     const std::string& aet);
+
+  void ParseLabels(std::set<std::string>& targetLabels,
+                   LabelsConstraint& targetConstraint,
+                   const Json::Value& serverConfig);
+  
+  void AddLabelsToFindRequest(Json::Value& request,
+                              const std::set<std::string>& labels,
+                              LabelsConstraint constraint);
+  
+  LabelsConstraint StringToLabelsConstraint(const std::string& s);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/StoreRequestHandler.cpp	Fri Apr 14 11:49:24 2023 +0200
@@ -0,0 +1,92 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2021-2023 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "StoreRequestHandler.h"
+
+#include "../../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
+#include "../../../../OrthancFramework/Sources/Logging.h"
+#include "../../../../OrthancFramework/Sources/OrthancException.h"
+#include "../../../../OrthancFramework/Sources/SerializationToolbox.h"
+
+#include "../Common/OrthancPluginCppWrapper.h"
+
+#include <dcmtk/dcmnet/diutil.h>
+
+
+uint16_t StoreRequestHandler::Handle(DcmDataset& dicom,
+                                     const std::string& remoteIp,
+                                     const std::string& remoteAet,
+                                     const std::string& calledAet)
+{
+  std::string buffer;
+
+  if (!Orthanc::FromDcmtkBridge::SaveToMemoryBuffer(buffer, dicom))
+  {
+    LOG(ERROR) << "Cannot write DICOM file to memory";
+    return STATUS_STORE_Error_CannotUnderstand;
+  }
+
+  Json::Value info;
+  if (!OrthancPlugins::RestApiPost(info, "/instances", buffer, false))
+  {
+    LOG(ERROR) << "Cannot store the DICOM file";
+    return STATUS_STORE_Refused_OutOfResources;
+  }
+
+  for (std::set<Orthanc::ResourceType>::const_iterator level = levels_.begin(); level != levels_.end(); ++level)
+  {
+    for (std::set<std::string>::const_iterator label = labels_.begin(); label != labels_.end(); ++label)
+    {
+      std::string uri;
+      switch (*level)
+      {
+        case Orthanc::ResourceType_Patient:
+          uri = "/patients/" + Orthanc::SerializationToolbox::ReadString(info, "ParentPatient") + "/labels/" + *label;
+          break;
+
+        case Orthanc::ResourceType_Study:
+          uri = "/studies/" + Orthanc::SerializationToolbox::ReadString(info, "ParentStudy") + "/labels/" + *label;
+          break;
+
+        case Orthanc::ResourceType_Series:
+          uri = "/series/" + Orthanc::SerializationToolbox::ReadString(info, "ParentSeries") + "/labels/" + *label;
+          break;
+
+        case Orthanc::ResourceType_Instance:
+          uri = "/instances/" + Orthanc::SerializationToolbox::ReadString(info, "ID") + "/labels/" + *label;
+          break;
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+
+      Json::Value tmp;
+      if (!OrthancPlugins::RestApiPut(tmp, uri, std::string(""), false))
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, "Cannot set label");
+      }
+    }
+  }
+
+  return STATUS_Success;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/StoreRequestHandler.h	Fri Apr 14 11:49:24 2023 +0200
@@ -0,0 +1,48 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2021-2023 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../../../OrthancFramework/Sources/DicomNetworking/IStoreRequestHandler.h"
+
+
+class StoreRequestHandler : public Orthanc::IStoreRequestHandler
+{
+private:
+  // Everything is constant, so no need for a mutex
+  const std::set<std::string> labels_;
+  const std::set<Orthanc::ResourceType> levels_;
+
+public:
+  StoreRequestHandler(const std::set<std::string>& labels,
+                      const std::set<Orthanc::ResourceType>& levels) :
+    labels_(labels),
+    levels_(levels)
+  {
+  }
+
+  virtual uint16_t Handle(DcmDataset& dicom,
+                          const std::string& remoteIp,
+                          const std::string& remoteAet,
+                          const std::string& calledAet) ORTHANC_OVERRIDE;
+};