changeset 77:94a9484d7f8f

fix security issues allowing to browse remote dicom servers + introduced UnitTests
author Alain Mazy <am@osimis.io>
date Wed, 15 Mar 2023 16:36:42 +0100
parents d301047ee3c4
children a2e5ea1869bd
files CMakeLists.txt NEWS Plugin/AuthorizationParserBase.cpp Plugin/AuthorizationParserBase.h Plugin/DefaultAuthorizationParser.cpp Plugin/DefaultConfiguration.json Plugin/Plugin.cpp Plugin/ResourceHierarchyCache.cpp Plugin/ResourceHierarchyCache.h UnitTestsSources/UnitTestsMain.cpp
diffstat 10 files changed, 386 insertions(+), 24 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Thu Mar 09 14:37:52 2023 +0100
+++ b/CMakeLists.txt	Wed Mar 15 16:36:42 2023 +0100
@@ -57,10 +57,16 @@
   
   link_libraries(${ORTHANC_FRAMEWORK_LIBRARIES})
 
+  set(USE_SYSTEM_GOOGLE_TEST ON CACHE BOOL "Use the system version of Google Test")
+  set(USE_GOOGLE_TEST_DEBIAN_PACKAGE OFF CACHE BOOL "Use the sources of Google Test shipped with libgtest-dev (Debian only)")
+  mark_as_advanced(USE_GOOGLE_TEST_DEBIAN_PACKAGE)
+  include(${CMAKE_SOURCE_DIR}/Resources/Orthanc/CMake/GoogleTestConfiguration.cmake)
+
 else()
   include(${ORTHANC_FRAMEWORK_ROOT}/../Resources/CMake/OrthancFrameworkParameters.cmake)
 
   set(ENABLE_LOCALE OFF)           # Disable support for locales (notably in Boost)
+  set(ENABLE_GOOGLE_TEST ON)
   set(ENABLE_MODULE_IMAGES OFF)
   set(ENABLE_MODULE_JOBS OFF)
   set(ENABLE_MODULE_DICOM OFF)
@@ -84,8 +90,6 @@
     message(FATAL_ERROR "Unsupported version of the Orthanc plugin SDK: ${ORTHANC_SDK_VERSION}")
   endif()
 
-
-include_directories(${CMAKE_SOURCE_DIR}/Resources/Orthanc//Sdk-1.3.1)
 else ()
   CHECK_INCLUDE_FILE_CXX(orthanc/OrthancCPlugin.h HAVE_ORTHANC_H)
   if (NOT HAVE_ORTHANC_H)
@@ -128,12 +132,21 @@
   DEFAULT_CONFIGURATION  ${CMAKE_SOURCE_DIR}/Plugin/DefaultConfiguration.json
   )
 
-  EmbedResources(
+EmbedResources(
   --no-upcase-check
   ${ADDITIONAL_RESOURCES}
   )
 
-add_library(OrthancAuthorization SHARED
+# As the embedded resources are shared by both the "UnitTests" and the
+# "OrthancAuthorization" targets, avoid race conditions in the code
+# generation by adding a target between them
+add_custom_target(
+  AutogeneratedTarget
+  DEPENDS 
+  ${AUTOGENERATED_SOURCES}
+  )
+
+set(PLUGIN_SOURCES
   ${CMAKE_SOURCE_DIR}/Plugin/AccessedResource.cpp
   ${CMAKE_SOURCE_DIR}/Plugin/AssociativeArray.cpp
   ${CMAKE_SOURCE_DIR}/Plugin/AuthorizationParserBase.cpp
@@ -150,8 +163,14 @@
   ${CMAKE_SOURCE_DIR}/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp
   ${ORTHANC_CORE_SOURCES}
   ${AUTOGENERATED_SOURCES}
-  )
+)
+
 
+add_library(OrthancAuthorization SHARED
+  ${PLUGIN_SOURCES}
+)
+
+add_dependencies(OrthancAuthorization AutogeneratedTarget)
 
 message("Setting the version of the plugin to ${ORTHANC_PLUGIN_VERSION}")
 add_definitions(-DORTHANC_PLUGIN_VERSION="${ORTHANC_PLUGIN_VERSION}")
@@ -165,3 +184,24 @@
   RUNTIME DESTINATION lib    # Destination for Windows
   LIBRARY DESTINATION share/orthanc/plugins    # Destination for Linux
   )
+
+add_executable(UnitTests
+  ${PLUGIN_SOURCES}
+  ${ORTHANC_CORE_SOURCES}
+  ${GOOGLE_TEST_SOURCES}
+  UnitTestsSources/UnitTestsMain.cpp
+  )
+
+# add_dependencies(UnitTests AutogeneratedTarget)
+
+target_include_directories(UnitTests PUBLIC ${ORTHANC_FRAMEWORK_ROOT})
+target_compile_definitions(UnitTests PUBLIC
+  -DHAS_ORTHANC_EXCEPTION=1
+  -DORTHANC_ENABLE_LOGGING_PLUGIN=1
+  -DBUILD_UNIT_TESTS=1
+  )
+
+target_link_libraries(UnitTests
+  ${GOOGLE_TEST_LIBRARIES}
+  # ${AUTOGENERATED_SOURCES}
+  )
\ No newline at end of file
--- a/NEWS	Thu Mar 09 14:37:52 2023 +0100
+++ b/NEWS	Wed Mar 15 16:36:42 2023 +0100
@@ -7,6 +7,10 @@
 * new GET "auth/user/profile" Rest API route to retrieve user permissions
 * new PUT "auth/tokens/{token-type}" Rest API route to create tokens
 * new POST "auth/tokens/decode" Rest API route to decode tokens
+* SECURITY FIX: in prior versions, it was possible to browse remote
+  dicom-web servers without being authenticated.  (The API routes
+  /dicom-web/servers/.../studies were unprotected).  The local
+  dicom-web server was correctly protected.
 
 
 2022-11-16 - v 0.4.1
--- a/Plugin/AuthorizationParserBase.cpp	Thu Mar 09 14:37:52 2023 +0100
+++ b/Plugin/AuthorizationParserBase.cpp	Wed Mar 15 16:36:42 2023 +0100
@@ -102,6 +102,18 @@
     target.push_back(AccessedResource(Orthanc::ResourceType_Study, study, studyDicomUid));
   }
 
+  void AuthorizationParserBase::AddDicomPatient(AccessedResources& target,
+                                                const std::string& patientId)
+  {
+    std::string patient;
+
+    if (!resourceHierarchy_->LookupOrthancId(patient, Orthanc::ResourceType_Patient, patientId))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
+    }
+
+    AddResourceInternal(target, Orthanc::ResourceType_Patient, patient);
+  }
   
   void AuthorizationParserBase::AddDicomSeries(AccessedResources& target,
                                                const std::string& studyDicomUid,
--- a/Plugin/AuthorizationParserBase.h	Thu Mar 09 14:37:52 2023 +0100
+++ b/Plugin/AuthorizationParserBase.h	Wed Mar 15 16:36:42 2023 +0100
@@ -23,6 +23,11 @@
 
 #include <Compatibility.h>  // For std::unique_ptr<>
 
+#if BUILD_UNIT_TESTS == 1
+#  include <gtest/gtest_prod.h>
+#endif
+
+
 namespace OrthancPlugins
 {
   class AuthorizationParserBase : public IAuthorizationParser
@@ -47,6 +52,9 @@
     void AddOrthancPatient(AccessedResources& target,
                            const std::string& orthancId);
 
+    void AddDicomPatient(AccessedResources& target,
+                         const std::string& patientId);
+
     void AddDicomStudy(AccessedResources& target,
                        const std::string& studyDicomUid);
     
@@ -67,5 +75,12 @@
     {
       resourceHierarchy_->Invalidate(level, id);
     }
+
+    FRIEND_TEST(DefaultAuthorizationParser, Parse);
+  protected:
+    ResourceHierarchyCache* GetResourceHierarchy()
+    {
+      return resourceHierarchy_.get();
+    }
   };
 }
--- a/Plugin/DefaultAuthorizationParser.cpp	Thu Mar 09 14:37:52 2023 +0100
+++ b/Plugin/DefaultAuthorizationParser.cpp	Wed Mar 15 16:36:42 2023 +0100
@@ -50,7 +50,7 @@
       "^" + tmp + "/studies/([.0-9]+)/series/([.0-9]+)/instances/([.0-9]+)(|/|/frames/.*)$");
 
     dicomWebQidoRsFind_ = boost::regex(
-      "^" + tmp + "/(studies|series|instances)\?(.*)$");
+      "^" + tmp + "/(studies|series|instances)$");
   }
 
 
@@ -134,40 +134,46 @@
     }
     else if (boost::regex_match(uri, what, dicomWebQidoRsFind_))
     {
-      std::string studyInstanceUid, seriesInstanceUid, sopInstanceUid;
+      std::string studyInstanceUid, seriesInstanceUid, sopInstanceUid, patientId;
 
       studyInstanceUid = Orthanc::HttpToolbox::GetArgument(getArguments, "0020000D", "");
       seriesInstanceUid = Orthanc::HttpToolbox::GetArgument(getArguments, "0020000E", "");
       sopInstanceUid = Orthanc::HttpToolbox::GetArgument(getArguments, "00080018", "");
+      patientId = Orthanc::HttpToolbox::GetArgument(getArguments, "00100010", "");
 
       if (!sopInstanceUid.empty() && !seriesInstanceUid.empty() && !studyInstanceUid.empty())
       {
         AddDicomInstance(target, studyInstanceUid, seriesInstanceUid, sopInstanceUid);
+        return true;
       }
       else if (!seriesInstanceUid.empty() && !studyInstanceUid.empty())
       {
         AddDicomSeries(target, studyInstanceUid, seriesInstanceUid);
+        return true;
       }
       else if (!studyInstanceUid.empty())
       {
         AddDicomStudy(target, studyInstanceUid);
+        return true;
       }
-      return true;
+      else if (!patientId.empty())
+      {
+        AddDicomPatient(target, patientId);
+        return true;
+      }
     }
-    else
-    {
-      // Unknown type of resource: Consider it as a system access
+
+    // Unknown type of resource: Consider it as a system access
 
-      // Remove the trailing slashes if need be
-      std::string s = uri;
-      while (!s.empty() &&
-             s[s.length() - 1] == '/')
-      {
-        s = s.substr(0, s.length() - 1);
-      }
-          
-      target.push_back(AccessedResource(AccessLevel_System, s, ""));
-      return true;
-    }        
+    // Remove the trailing slashes if need be
+    std::string s = uri;
+    while (!s.empty() &&
+            s[s.length() - 1] == '/')
+    {
+      s = s.substr(0, s.length() - 1);
+    }
+        
+    target.push_back(AccessedResource(AccessLevel_System, s, ""));
+    return true;
   }
 }
--- a/Plugin/DefaultConfiguration.json	Thu Mar 09 14:37:52 2023 +0100
+++ b/Plugin/DefaultConfiguration.json	Wed Mar 15 16:36:42 2023 +0100
@@ -50,6 +50,7 @@
         // The default configuration is suitable for Orthanc-Explorer-2 (see TBD sample)
         "Permissions" : [
             ["post", "^/auth/tokens/decode$", ""],
+            ["post", "^/tools/lookup$", ""], // currently used to authorize downloads in Stone (to map the StudyInstanceUID into an OrthancID.  Not ideal -> we should define a new API that has the resource ID in the path to be able to check it at resource level) but, on another hand, you do not get any Patient information from this route
 
             // elemental browsing in OE2
             ["post", "^/tools/find$", "all|view"],
@@ -83,7 +84,14 @@
 
             // modifications/anonymization
             ["post", "^/(patients|studies|series|instances)/([a-f0-9-]+)/modify(.*)$", "all|modify"],
-            ["post", "^/(patients|studies|series|instances)/([a-f0-9-]+)/anonymize(.*)$", "all|anonymize"]
+            ["post", "^/(patients|studies|series|instances)/([a-f0-9-]+)/anonymize(.*)$", "all|anonymize"],
+
+            // deletes
+            ["delete" , "^/(patients|studies|series|instances)/([a-f0-9-]+)$", "all|delete"],
+
+            // settings
+            ["put", "^/tools/log-level$", "all|settings"],
+            ["get", "^/tools/log-level$", "all|settings"]
         ]
     }
 }
\ No newline at end of file
--- a/Plugin/Plugin.cpp	Thu Mar 09 14:37:52 2023 +0100
+++ b/Plugin/Plugin.cpp	Wed Mar 15 16:36:42 2023 +0100
@@ -745,7 +745,6 @@
           {
             uncheckedFolders_.push_back("/stone-webviewer/");
             uncheckedResources_.insert("/system");        // for Stone to check that Orthanc is the server providing the data
-            uncheckedResources_.insert("/tools/lookup");  // for Downloads  (we consider that having access to tools/lookup can not give information about other patients/studies since it only return IDs, no patient data)
 
             tokens_.insert(OrthancPlugins::Token(OrthancPlugins::TokenType_HttpHeader, "Authorization"));
           }
--- a/Plugin/ResourceHierarchyCache.cpp	Thu Mar 09 14:37:52 2023 +0100
+++ b/Plugin/ResourceHierarchyCache.cpp	Wed Mar 15 16:36:42 2023 +0100
@@ -193,4 +193,21 @@
       return false;
     }
   }
+
+  void ResourceHierarchyCache::AddOrthancDicomMapping(Orthanc::ResourceType level,
+                                                      const std::string& orthancId,
+                                                      const std::string& dicomUid)
+  {
+    dicomToOrthanc_->Store(ComputeKey(level, dicomUid), orthancId, 0 /* no expiration */);
+    orthancToDicom_->Store(ComputeKey(level, orthancId), dicomUid, 0 /* no expiration */);
+  }
+
+  void ResourceHierarchyCache::AddParentLink(Orthanc::ResourceType childLevel,
+                                             const std::string& childOrthancId,
+                                             const std::string& parentOrthancId)
+  {
+    cache_->Store(ComputeKey(childLevel, childOrthancId), parentOrthancId, 0 /* no expiration */);
+  }
+
+
 }
--- a/Plugin/ResourceHierarchyCache.h	Thu Mar 09 14:37:52 2023 +0100
+++ b/Plugin/ResourceHierarchyCache.h	Wed Mar 15 16:36:42 2023 +0100
@@ -26,6 +26,10 @@
 
 #include <orthanc/OrthancCPlugin.h>
 
+#if BUILD_UNIT_TESTS == 1
+#  include <gtest/gtest_prod.h>
+#endif
+
 namespace OrthancPlugins
 {
   class ResourceHierarchyCache : public boost::noncopyable
@@ -84,5 +88,16 @@
     bool LookupOrthancId(std::string& target,
                          Orthanc::ResourceType level,
                          const std::string& dicomUid);
+
+    FRIEND_TEST(DefaultAuthorizationParser, Parse);
+  protected:
+    void AddOrthancDicomMapping(Orthanc::ResourceType level,
+                                const std::string& orthancId,
+                                const std::string& dicomUid);
+
+    void AddParentLink(Orthanc::ResourceType childLevel,
+                       const std::string& childOrthancId,
+                       const std::string& parentOrthancId);
+
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/UnitTestsSources/UnitTestsMain.cpp	Wed Mar 15 16:36:42 2023 +0100
@@ -0,0 +1,246 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2021 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero 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
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include <gtest/gtest.h>
+#include <boost/lexical_cast.hpp>
+#include <boost/algorithm/string/predicate.hpp>
+
+#include "../Plugin/DefaultAuthorizationParser.h"
+#include "../Plugin/AssociativeArray.h"
+#include "../Plugin/AccessedResource.h"
+#include "../Plugin/MemoryCache.h"
+#include "../Plugin/PermissionParser.h"
+#include "../Plugin/ResourceHierarchyCache.h"
+
+using namespace OrthancPlugins;
+
+std::string instanceOrthancId = "44444444-44444444-44444444-44444444-44444444";
+std::string seriesOrthancId   = "33333333-33333333-33333333-33333333-33333333";
+std::string studyOrthancId    = "22222222-22222222-22222222-22222222-22222222";
+std::string patientOrthancId  = "11111111-11111111-11111111-11111111-11111111";
+
+std::string instanceDicomUid = "4.4";
+std::string seriesDicomUid   = "3.3";
+std::string studyDicomUid    = "2.2";
+std::string patientDicomUid  = "PATIENT.1";
+
+bool IsAccessing(const IAuthorizationParser::AccessedResources& accesses, AccessLevel level, const std::string& orthancId)
+{
+  for (IAuthorizationParser::AccessedResources::const_iterator it = accesses.begin(); it != accesses.end(); ++it)
+  {
+    if (it->GetLevel() == level && it->GetOrthancId() == orthancId)
+    {
+      return true;
+    }
+  }
+  return false;
+}
+
+namespace OrthancPlugins
+{
+  // The namespace is necessary for friend classes to work
+  // http://code.google.com/p/googletest/wiki/AdvancedGuide#Private_Class_Members
+
+TEST(DefaultAuthorizationParser, Parse)
+{
+  MemoryCache::Factory factory(10);
+  DefaultAuthorizationParser parser(factory, "/dicom-web/");
+  ResourceHierarchyCache* cache = parser.GetResourceHierarchy();
+
+  cache->AddOrthancDicomMapping(Orthanc::ResourceType_Instance, instanceOrthancId, instanceDicomUid);
+  cache->AddOrthancDicomMapping(Orthanc::ResourceType_Series, seriesOrthancId, seriesDicomUid);
+  cache->AddOrthancDicomMapping(Orthanc::ResourceType_Study, studyOrthancId, studyDicomUid);
+  cache->AddOrthancDicomMapping(Orthanc::ResourceType_Patient, patientOrthancId, patientDicomUid);
+
+  cache->AddParentLink(Orthanc::ResourceType_Instance, instanceOrthancId, seriesOrthancId);
+  cache->AddParentLink(Orthanc::ResourceType_Series, seriesOrthancId, studyOrthancId);
+  cache->AddParentLink(Orthanc::ResourceType_Study, studyOrthancId, patientOrthancId);
+
+  IAuthorizationParser::AccessedResources accesses;
+  AssociativeArray noGetArguments(0, NULL, NULL, false);
+
+  accesses.clear();
+  parser.Parse(accesses, "/studies/22222222-22222222-22222222-22222222-22222222/", noGetArguments.GetMap());
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Study, studyOrthancId));
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Patient, patientOrthancId));
+
+  accesses.clear();
+  parser.Parse(accesses, "/studies/22222222-22222222-22222222-22222222-22222222/instances", noGetArguments.GetMap());
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Study, studyOrthancId));
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Patient, patientOrthancId));
+
+  accesses.clear();
+  parser.Parse(accesses, "/studies/22222222-22222222-22222222-22222222-22222222/archive", noGetArguments.GetMap());
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Study, studyOrthancId));
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Patient, patientOrthancId));
+
+  accesses.clear();
+  parser.Parse(accesses, "/osimis-viewer/studies/22222222-22222222-22222222-22222222-22222222/archive", noGetArguments.GetMap());
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Study, studyOrthancId));
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Patient, patientOrthancId));
+
+  accesses.clear();
+  parser.Parse(accesses, "/series/33333333-33333333-33333333-33333333-33333333/", noGetArguments.GetMap());
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Series, seriesOrthancId));
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Study, studyOrthancId));
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Patient, patientOrthancId));
+
+  accesses.clear();
+  parser.Parse(accesses, "/series/33333333-33333333-33333333-33333333-33333333/media", noGetArguments.GetMap());
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Series, seriesOrthancId));
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Study, studyOrthancId));
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Patient, patientOrthancId));
+
+  accesses.clear();
+  parser.Parse(accesses, "/series/33333333-33333333-33333333-33333333-33333333/modify", noGetArguments.GetMap());
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Series, seriesOrthancId));
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Study, studyOrthancId));
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Patient, patientOrthancId));
+
+  accesses.clear();
+  parser.Parse(accesses, "/web-viewer/series/33333333-33333333-33333333-33333333-33333333", noGetArguments.GetMap());
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Series, seriesOrthancId));
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Study, studyOrthancId));
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Patient, patientOrthancId));
+
+  accesses.clear();
+  parser.Parse(accesses, "/osimis-viewer/series/33333333-33333333-33333333-33333333-33333333", noGetArguments.GetMap());
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Series, seriesOrthancId));
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Study, studyOrthancId));
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Patient, patientOrthancId));
+
+  accesses.clear();
+  parser.Parse(accesses, "/instances/44444444-44444444-44444444-44444444-44444444/file", noGetArguments.GetMap());
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Instance, instanceOrthancId));
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Series, seriesOrthancId));
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Study, studyOrthancId));
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Patient, patientOrthancId));
+
+  accesses.clear();
+  parser.Parse(accesses, "/web-viewer/instances/jpeg95-44444444-44444444-44444444-44444444-44444444_0", noGetArguments.GetMap());
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Instance, instanceOrthancId));
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Series, seriesOrthancId));
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Study, studyOrthancId));
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Patient, patientOrthancId));
+
+  accesses.clear();
+  parser.Parse(accesses, "/osimis-viewer/images/44444444-44444444-44444444-44444444-44444444/0/high-quality", noGetArguments.GetMap());
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Instance, instanceOrthancId));
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Series, seriesOrthancId));
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Study, studyOrthancId));
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Patient, patientOrthancId));
+
+  accesses.clear();
+  parser.Parse(accesses, "/system", noGetArguments.GetMap());
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_System, "/system"));
+
+
+  ///////////////////////// dicom-web
+  accesses.clear();
+  parser.Parse(accesses, "/dicom-web/studies/2.2", noGetArguments.GetMap());
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Study, studyOrthancId));
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Patient, patientOrthancId));
+
+  accesses.clear();
+  parser.Parse(accesses, "/dicom-web/studies/2.2/series/3.3", noGetArguments.GetMap());
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Series, seriesOrthancId));
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Study, studyOrthancId));
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Patient, patientOrthancId));
+
+  accesses.clear();
+  parser.Parse(accesses, "/dicom-web/studies/2.2/series/3.3/instances/4.4", noGetArguments.GetMap());
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Instance, instanceOrthancId));
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Series, seriesOrthancId));
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Study, studyOrthancId));
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Patient, patientOrthancId));
+
+  accesses.clear();
+  parser.Parse(accesses, "/dicom-web/studies/2.2/series/3.3/instances/4.4/frames/0", noGetArguments.GetMap());
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Instance, instanceOrthancId));
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Series, seriesOrthancId));
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Study, studyOrthancId));
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Patient, patientOrthancId));
+
+  {
+    accesses.clear();
+    const char* getKeys[] = {"0020000D"};
+    const char* getValues[] = {"2.2"};
+    AssociativeArray getArguments(1, getKeys, getValues, false);
+    parser.Parse(accesses, "/dicom-web/studies", getArguments.GetMap());
+    ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Study, studyOrthancId));
+    ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Patient, patientOrthancId));
+  }
+  {
+    accesses.clear();
+    const char* getKeys[] = {"0020000D", "0020000E"};
+    const char* getValues[] = {"2.2", "3.3"};
+    AssociativeArray getArguments(2, getKeys, getValues, false);
+    parser.Parse(accesses, "/dicom-web/series", getArguments.GetMap());
+    ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Series, seriesOrthancId));
+    ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Study, studyOrthancId));
+    ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Patient, patientOrthancId));
+  }
+  {
+    accesses.clear();
+    const char* getKeys[] = {"0020000D", "00080018", "0020000E"};
+    const char* getValues[] = {"2.2", "4.4", "3.3", };
+    AssociativeArray getArguments(3, getKeys, getValues, false);
+    parser.Parse(accesses, "/dicom-web/studies", getArguments.GetMap());
+    ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Instance, instanceOrthancId));
+    ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Series, seriesOrthancId));
+    ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Study, studyOrthancId));
+    ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Patient, patientOrthancId));
+  }
+  {
+    accesses.clear();
+    const char* getKeys[] = {"00100010"};
+    const char* getValues[] = {"PATIENT.1"};
+    AssociativeArray getArguments(1, getKeys, getValues, false);
+    parser.Parse(accesses, "/dicom-web/studies", getArguments.GetMap());
+    ASSERT_TRUE(IsAccessing(accesses, AccessLevel_Patient, patientOrthancId));
+  }
+
+  { // qido with no arguments = search all => system resource
+    accesses.clear();
+    parser.Parse(accesses, "/dicom-web/studies", noGetArguments.GetMap());
+    ASSERT_TRUE(IsAccessing(accesses, AccessLevel_System, "/dicom-web/studies"));
+  }
+
+  accesses.clear();
+  parser.Parse(accesses, "/dicom-web/servers", noGetArguments.GetMap());
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_System, "/dicom-web/servers"));
+
+  accesses.clear();
+  parser.Parse(accesses, "/dicom-web/info", noGetArguments.GetMap());
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_System, "/dicom-web/info"));
+
+  accesses.clear();
+  parser.Parse(accesses, "/dicom-web/servers/test/qido", noGetArguments.GetMap());
+  ASSERT_TRUE(IsAccessing(accesses, AccessLevel_System, "/dicom-web/servers/test/qido"));
+
+}
+}
+
+int main(int argc, char **argv)
+{
+  ::testing::InitGoogleTest(&argc, argv);
+  return RUN_ALL_TESTS();
+}