changeset 149:423531fb1200

SINGLE_RESOURCE_PATTERNS to facilitate api-key support
author Alain Mazy <am@osimis.io>
date Thu, 15 Feb 2024 16:30:21 +0100
parents 20c638fa8b07
children 9be1ee2b8fe1 e7cee71a2f86
files NEWS Plugin/DefaultAuthorizationParser.cpp Plugin/DefaultAuthorizationParser.h Plugin/DefaultConfiguration.json Plugin/IAuthorizationParser.h Plugin/PermissionParser.cpp Plugin/PermissionParser.h Plugin/Plugin.cpp
diffstat 8 files changed, 76 insertions(+), 32 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Thu Feb 15 12:04:28 2024 +0100
+++ b/NEWS	Thu Feb 15 16:30:21 2024 +0100
@@ -1,7 +1,13 @@
 Pending changes in the mainline
 ===============================
 
-* Added new default permissions for dicom-web STOW-RS, QIDO-RS and WADO-RS.
+* Added new default permissions "SINGLE_RESOURCE_PATTERNS" and 
+  reorganized the permissions accordingly.  
+  This notably facilitate accessing a single resource with a user token.  
+  The plugins first check that the user has the right permissions to access the route 
+  and then, the plugins check if the study has one of the authorized_labels
+  of the user.  This notably improves user token handling outside of OE2
+  e.g when using api-key.
 
 
 2023-12-19 - v 0.6.2
--- a/Plugin/DefaultAuthorizationParser.cpp	Thu Feb 15 12:04:28 2024 +0100
+++ b/Plugin/DefaultAuthorizationParser.cpp	Thu Feb 15 16:30:21 2024 +0100
@@ -57,7 +57,20 @@
       "^" + tmp + "/(studies|series|instances)(|/)$");
   }
 
-  bool DefaultAuthorizationParser::IsListOfResources(const std::string& uri)
+  void DefaultAuthorizationParser::GetSingleResourcePatterns(std::vector<boost::regex>& patterns) const
+  {
+    patterns.push_back(resourcesPattern_);
+    patterns.push_back(seriesPattern_);
+    patterns.push_back(instancesPattern_);
+    patterns.push_back(osimisViewerSeries_);
+    patterns.push_back(osimisViewerImages_);
+    patterns.push_back(osimisViewerStudies_);
+    patterns.push_back(dicomWebStudies_);
+    patterns.push_back(dicomWebSeries_);
+    patterns.push_back(dicomWebInstances_);
+  }
+
+  bool DefaultAuthorizationParser::IsListOfResources(const std::string& uri) const
   {
     if (boost::regex_match(uri, listOfResourcesPattern_))
     {
--- a/Plugin/DefaultAuthorizationParser.h	Thu Feb 15 12:04:28 2024 +0100
+++ b/Plugin/DefaultAuthorizationParser.h	Thu Feb 15 16:30:21 2024 +0100
@@ -28,7 +28,7 @@
   class DefaultAuthorizationParser : public AuthorizationParserBase
   { 
   private:
-    boost::mutex mutex_; 
+    mutable boost::mutex mutex_; 
     boost::regex resourcesPattern_;
     boost::regex seriesPattern_;
     boost::regex instancesPattern_;
@@ -52,6 +52,8 @@
                        const std::string& uri,
                        const std::map<std::string, std::string>& getArguments);
 
-    virtual bool IsListOfResources(const std::string& uri);
+    virtual bool IsListOfResources(const std::string& uri) const;
+
+    virtual void GetSingleResourcePatterns(std::vector<boost::regex>& patterns) const;
   };
 }
--- a/Plugin/DefaultConfiguration.json	Thu Feb 15 12:04:28 2024 +0100
+++ b/Plugin/DefaultConfiguration.json	Thu Feb 15 16:30:21 2024 +0100
@@ -59,12 +59,13 @@
             // elemental browsing in OE2
             ["post", "^/tools/find$", "all|view"],
             ["get" , "^/(patients|studies|series|instances)(|/)", "all|view"],
-            ["get" , "^/(patients|studies|series|instances)/([a-f0-9-]+)$", "all|view"],
-            ["get" , "^/(patients|studies|series|instances)/([a-f0-9-]+)/(studies|study|series|instances)$", "all|view"],
-            ["get" , "^/instances/([a-f0-9-]+)/(tags|header)$", "all|view"],
             ["get" , "^/statistics$", "all|view"],
             ["get" , "^/changes$", "all|view"],
 
+            // single resources patterns (SINGLE_RESOURCE_PATTERNS is an alias for all single resource patterns defined in https://orthanc.uclouvain.be/hg/orthanc-authorization/file/tip/Plugin/DefaultAuthorizationParser.cpp)
+            // (a user must have access to the route + have an authorized label to access the resource)
+            ["get" , "SINGLE_RESOURCE_PATTERNS", "all|view"],
+
             // create links to open viewer or download resources
             ["put", "^/auth/tokens/(viewer-instant-link|meddream-instant-link)$", "all|view"],
             ["put", "^/auth/tokens/(download-instant-link)$", "all|download"],
@@ -91,10 +92,6 @@
 
             // DICOMWeb QIDO-RS            
             ["get" , "^/DICOM_WEB_ROOT/(studies|series|instances)(|/)$", "all|view"],
-            // DICOMWeb WADO-RS (a user must have access to the route + have an authorized label)
-            ["get" , "^/DICOM_WEB_ROOT/studies/([.0-9]+)(|/series|/metadata)(|/)$", "all|view"],
-            ["get" , "^/DICOM_WEB_ROOT/studies/([.0-9]+)/series/([.0-9]+)(|/instances|/rendered|/metadata)(|/)$", "all|view"],
-            ["get" , "^/DICOM_WEB_ROOT/studies/([.0-9]+)/series/([.0-9]+)/instances/([.0-9]+)(|/|/frames/.*|/rendered|/metadata|/bulk/.*)(|/)$", "all|view"],
 
             // modifications/anonymization
             ["post", "^/(patients|studies|series|instances)/([a-f0-9-]+)/(modify|merge)(.*)$", "all|modify"],
@@ -102,7 +99,6 @@
 
             // labels
             ["get", "^/tools/labels$", "all|view|edit-labels"],
-            ["get" , "^/(patients|studies|series|instances)/([a-f0-9-]+)/labels$", "all|view"],
             ["put" , "^/(patients|studies|series|instances)/([a-f0-9-]+)/labels/(.*)$", "all|edit-labels"],
             ["delete" , "^/(patients|studies|series|instances)/([a-f0-9-]+)/labels/(.*)$", "all|edit-labels"],
 
--- a/Plugin/IAuthorizationParser.h	Thu Feb 15 12:04:28 2024 +0100
+++ b/Plugin/IAuthorizationParser.h	Thu Feb 15 16:30:21 2024 +0100
@@ -21,8 +21,10 @@
 #include "AccessedResource.h"
 
 #include <boost/noncopyable.hpp>
+#include <boost/regex.hpp>
 #include <list>
 #include <map>
+#include <vector>
 
 namespace OrthancPlugins
 {
@@ -43,6 +45,8 @@
                        const std::string& uri,
                        const std::map<std::string, std::string>& getArguments) = 0;
 
-    virtual bool IsListOfResources(const std::string& uri) = 0;
+    virtual bool IsListOfResources(const std::string& uri) const = 0;
+
+    virtual void GetSingleResourcePatterns(std::vector<boost::regex>& patterns) const = 0;
   };
 }
--- a/Plugin/PermissionParser.cpp	Thu Feb 15 12:04:28 2024 +0100
+++ b/Plugin/PermissionParser.cpp	Thu Feb 15 16:30:21 2024 +0100
@@ -70,7 +70,7 @@
   {
   }
 
-  void PermissionParser::Add(const Json::Value& configuration)
+  void PermissionParser::Add(const Json::Value& configuration, const IAuthorizationParser* authorizationParser)
   {
     if (configuration.type() != Json::arrayValue)
     {
@@ -85,11 +85,28 @@
         throw Orthanc::OrthancException(Orthanc::ErrorCode_BadParameterType, "Permissions elements should be an array of min size 3.");
       }
 
-      Add(permission[0].asString(),    // 0 = HTTP method
-          permission[1].asString(),    // 1 = pattern
-          permission[2].asString()     // 2 = list of | separated permissions (no space)
-                                       // 3 = optional comment
-      );
+      if (permission[1].asString() == "SINGLE_RESOURCE_PATTERNS")
+      {
+        std::vector<boost::regex> singleResourcePatterns;
+        authorizationParser->GetSingleResourcePatterns(singleResourcePatterns);
+
+        for (std::vector<boost::regex>::const_iterator it = singleResourcePatterns.begin(); it != singleResourcePatterns.end(); ++it)
+        {
+          Add(permission[0].asString(),    // 0 = HTTP method
+              it->str(),                   // 1 = pattern
+              permission[2].asString()     // 2 = list of | separated permissions (no space)
+                                           // 3 = optional comment
+          );
+        }
+      }
+      else
+      {
+        Add(permission[0].asString(),    // 0 = HTTP method
+            permission[1].asString(),    // 1 = pattern
+            permission[2].asString()     // 2 = list of | separated permissions (no space)
+                                         // 3 = optional comment
+        );
+      }
     }
 
   }
--- a/Plugin/PermissionParser.h	Thu Feb 15 12:04:28 2024 +0100
+++ b/Plugin/PermissionParser.h	Thu Feb 15 16:30:21 2024 +0100
@@ -50,7 +50,7 @@
              const std::string& patternRegex,
              const std::string& permission);
 
-    void Add(const Json::Value& configuration);
+    void Add(const Json::Value& configuration, const IAuthorizationParser* authorizationParser);
 
     bool Parse(std::set<std::string>& permissions,
                std::string& matchedPattern,
--- a/Plugin/Plugin.cpp	Thu Feb 15 12:04:28 2024 +0100
+++ b/Plugin/Plugin.cpp	Thu Feb 15 16:30:21 2024 +0100
@@ -32,6 +32,8 @@
 
 
 // Configuration of the authorization plugin
+static bool resourceTokensEnabled_ = false;
+static bool userTokensEnabled_ = false;
 static std::unique_ptr<OrthancPlugins::IAuthorizationParser> authorizationParser_;
 static std::unique_ptr<OrthancPlugins::IAuthorizationService> authorizationService_;
 static std::unique_ptr<OrthancPlugins::PermissionParser> permissionParser_;
@@ -121,8 +123,8 @@
 
     if (authorizationParser_->IsListOfResources(uri))
     {
-      granted = false;
-      return true; // if a user does not have access to all labels, he can not have access to a list of resources
+      granted = false;  // if a user does not have access to all labels, he can not have access to a list of resources
+      return true; 
     }
 
     // Loop over all the accessed resources to ensure access is
@@ -365,7 +367,7 @@
     // If we get till here, it means that we have a resource token -> check that the resource is accessible
     ////////////////////////////////////////////////////////////////
 
-    if (authorizationParser_.get() != NULL &&
+    if (resourceTokensEnabled_ &&
         authorizationService_.get() != NULL)
     {
       // Parse the resources that are accessed through this URI
@@ -1137,20 +1139,23 @@
           pluginConfiguration.LookupStringValue(urlUserProfile, WEB_SERVICE_USER_PROFILE);
         }
 
+        authorizationParser_.reset(new OrthancPlugins::DefaultAuthorizationParser(factory, dicomWebRoot));
+
         if (!urlTokenValidation.empty())
         {
-          LOG(WARNING) << "Authorization plugin: url defined for Token Validation: " << urlTokenValidation;
-          authorizationParser_.reset
-            (new OrthancPlugins::DefaultAuthorizationParser(factory, dicomWebRoot));
+          LOG(WARNING) << "Authorization plugin: url defined for Token Validation: " << urlTokenValidation << ", resource tokens validation is enabled";
+          resourceTokensEnabled_ = true;
         }
         else
         {
-          LOG(WARNING) << "Authorization plugin: no url defined for Token Validation";
+          LOG(WARNING) << "Authorization plugin: no url defined for Token Validation, resource tokens validation is disabled";
+          resourceTokensEnabled_ = false;
         }
 
         if (!urlUserProfile.empty())
         {
-          LOG(WARNING) << "Authorization plugin: url defined for User Profile: " << urlUserProfile;
+          LOG(WARNING) << "Authorization plugin: url defined for User Profile: " << urlUserProfile << ", user tokens validation is enabled";
+          userTokensEnabled_ = true;
           
           static const char* PERMISSIONS = "Permissions";        
           if (!pluginConfiguration.GetJson().isMember(PERMISSIONS))
@@ -1161,11 +1166,12 @@
           permissionParser_.reset
             (new OrthancPlugins::PermissionParser(dicomWebRoot, oe2Root));
 
-          permissionParser_->Add(pluginConfiguration.GetJson()[PERMISSIONS]);
+          permissionParser_->Add(pluginConfiguration.GetJson()[PERMISSIONS], authorizationParser_.get());
         }
         else
         {
-          LOG(WARNING) << "Authorization plugin: no url defined for User Profile";
+          LOG(WARNING) << "Authorization plugin: no url defined for User Profile" << ", user tokens validation is disabled";
+          userTokensEnabled_ = false;
         }
 
         if (!urlTokenCreationBase.empty())
@@ -1177,7 +1183,7 @@
           LOG(WARNING) << "Authorization plugin: no base url defined for Token Creation";
         }
 
-        if (authorizationParser_.get() == NULL && permissionParser_.get() == NULL)
+        if (!resourceTokensEnabled_ && permissionParser_.get() == NULL)
         {
           if (hasBasicAuthEnabled)
           {
@@ -1326,7 +1332,7 @@
           OrthancPlugins::RegisterRestCallback<CreateToken>("/auth/tokens/(.*)", true);
         }
 
-        if (authorizationParser_.get() != NULL || permissionParser_.get() != NULL)
+        if (resourceTokensEnabled_ || userTokensEnabled_)
         {
           if (hasBasicAuthEnabled)
           {