changeset 71:30fb3ce960d9

configurable user permissions
author Alain Mazy <am@osimis.io>
date Wed, 22 Feb 2023 13:13:38 +0100
parents 786b202ef24e
children e381ba725669
files CMakeLists.txt NEWS Plugin/AuthorizationWebService.cpp Plugin/AuthorizationWebService.h Plugin/BaseAuthorizationService.h Plugin/CachedAuthorizationService.cpp Plugin/CachedAuthorizationService.h Plugin/DefaultConfiguration.json Plugin/Enumerations.cpp Plugin/IAuthorizationService.h Plugin/PermissionParser.cpp Plugin/PermissionParser.h Plugin/Plugin.cpp Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h
diffstat 15 files changed, 766 insertions(+), 116 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Tue Feb 21 09:23:47 2023 +0100
+++ b/CMakeLists.txt	Wed Feb 22 13:13:38 2023 +0100
@@ -124,6 +124,15 @@
   -DORTHANC_ENABLE_LOGGING_PLUGIN=1
   )
 
+set(ADDITIONAL_RESOURCES
+  DEFAULT_CONFIGURATION  ${CMAKE_SOURCE_DIR}/Plugin/DefaultConfiguration.json
+  )
+
+  EmbedResources(
+  --no-upcase-check
+  ${ADDITIONAL_RESOURCES}
+  )
+
 add_library(OrthancAuthorization SHARED
   ${CMAKE_SOURCE_DIR}/Plugin/AccessedResource.cpp
   ${CMAKE_SOURCE_DIR}/Plugin/AssociativeArray.cpp
@@ -134,6 +143,7 @@
   ${CMAKE_SOURCE_DIR}/Plugin/Enumerations.cpp
   ${CMAKE_SOURCE_DIR}/Plugin/MemoryCache.cpp
   ${CMAKE_SOURCE_DIR}/Plugin/OrthancResource.cpp
+  ${CMAKE_SOURCE_DIR}/Plugin/PermissionParser.cpp
   ${CMAKE_SOURCE_DIR}/Plugin/Plugin.cpp
   ${CMAKE_SOURCE_DIR}/Plugin/ResourceHierarchyCache.cpp
   ${CMAKE_SOURCE_DIR}/Plugin/Token.cpp
--- a/NEWS	Tue Feb 21 09:23:47 2023 +0100
+++ b/NEWS	Wed Feb 22 13:13:38 2023 +0100
@@ -1,5 +1,7 @@
 * new "orthanc-explorer-2" StandardConfigurations
 * new "auth/user-profile" Rest API route
+* new user-permission based authorization model.  This is enabled if you
+  define the new "WebServiceUserProfileUrl" configuration.
 
 2022-11-16 - v 0.4.1
 ====================
--- a/Plugin/AuthorizationWebService.cpp	Tue Feb 21 09:23:47 2023 +0100
+++ b/Plugin/AuthorizationWebService.cpp	Wed Feb 22 13:13:38 2023 +0100
@@ -23,9 +23,15 @@
 #include <Logging.h>
 #include <Toolbox.h>
 #include <HttpClient.h>
+#include <algorithm>
 
 namespace OrthancPlugins
 {
+  static const char* GRANTED = "granted";
+  static const char* VALIDITY = "validity";
+  static const char* PERMISSIONS = "permissions";
+
+
   bool AuthorizationWebService::IsGrantedInternal(unsigned int& validity,
                                                   OrthancPluginHttpMethod method,
                                                   const AccessedResource& access,
@@ -118,9 +124,6 @@
     Json::Value answer;
     authClient.ApplyAndThrowException(answer);
 
-    static const char* GRANTED = "granted";
-    static const char* VALIDITY = "validity";
-      
     if (answer.type() != Json::objectValue ||
         !answer.isMember(GRANTED) ||
         answer[GRANTED].type() != Json::booleanValue ||
@@ -165,9 +168,10 @@
     identifier_ = webServiceIdentifier;
   }
 
-  bool AuthorizationWebService::GetUserProfile(Json::Value& profile /* out */,
-                                               const Token& token,
-                                               const std::string& tokenValue)
+  bool AuthorizationWebService::GetUserProfileInternal(unsigned int& validity,
+                                                       Json::Value& profile /* out */,
+                                                       const Token* token,
+                                                       const std::string& tokenValue)
   {
     if (userProfileUrl_.empty())
     {
@@ -184,8 +188,11 @@
 
     Json::Value body;
 
-    body["token-key"] = token.GetKey();
-    body["token-value"] = tokenValue;
+    if (token != NULL)
+    {
+      body["token-key"] = token->GetKey();
+      body["token-value"] = tokenValue;
+    }
 
     if (!identifier_.empty())
     {
@@ -209,6 +216,16 @@
       authClient.SetTimeout(10);
 
       authClient.ApplyAndThrowException(profile);
+
+      if (profile.isMember("validity"))
+      {
+        validity = profile["validity"].asInt();
+      }
+      else
+      {
+        validity = 0;
+      }
+
       return true;
     }
     catch (Orthanc::OrthancException& ex)
@@ -217,4 +234,39 @@
     }
   }
 
+  bool AuthorizationWebService::HasUserPermissionInternal(unsigned int& validity,
+                                                          const std::string& permission,
+                                                          const Token* token,
+                                                          const std::string& tokenValue)
+  {
+    Json::Value profile;
+
+
+    if (GetUserProfileInternal(validity, profile, token, tokenValue))
+    {
+      if (profile.type() != Json::objectValue ||
+          !profile.isMember(PERMISSIONS) ||
+          !profile.isMember(VALIDITY) ||
+          profile[PERMISSIONS].type() != Json::arrayValue ||
+          profile[VALIDITY].type() != Json::intValue)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol,
+                                        "Syntax error in the result of the Web service");
+      }
+
+      validity = profile[VALIDITY].asUInt();
+
+      Json::Value& permissions = profile[PERMISSIONS];
+      for (Json::ArrayIndex i = 0; i < permissions.size(); ++i)
+      {
+        if (permission == permissions[i].asString())
+        {
+          return true;
+        }
+      }
+    }
+
+    return false;
+  }
+
 }
--- a/Plugin/AuthorizationWebService.h	Tue Feb 21 09:23:47 2023 +0100
+++ b/Plugin/AuthorizationWebService.h	Wed Feb 22 13:13:38 2023 +0100
@@ -18,11 +18,12 @@
 
 #pragma once
 
-#include "IAuthorizationService.h"
+#include "BaseAuthorizationService.h"
+#include <Compatibility.h>
 
 namespace OrthancPlugins
 {
-  class AuthorizationWebService : public IAuthorizationService
+  class AuthorizationWebService : public BaseAuthorizationService
   {
   private:
     std::string url_;
@@ -31,12 +32,23 @@
     std::string identifier_;
     std::string userProfileUrl_;
 
-    bool IsGrantedInternal(unsigned int& validity,
+  protected:
+    virtual bool IsGrantedInternal(unsigned int& validity,
                            OrthancPluginHttpMethod method,
                            const AccessedResource& access,
                            const Token* token,
-                           const std::string& tokenValue);
+                           const std::string& tokenValue) ORTHANC_OVERRIDE;
     
+    virtual bool GetUserProfileInternal(unsigned int& validity,
+                                Json::Value& profile /* out */,
+                                const Token* token,
+                                const std::string& tokenValue) ORTHANC_OVERRIDE;
+
+    virtual bool HasUserPermissionInternal(unsigned int& validity,
+                                   const std::string& permission,
+                                   const Token* token,
+                                   const std::string& tokenValue) ORTHANC_OVERRIDE;
+  
   public:
     AuthorizationWebService(const std::string& url) :
       url_(url)
@@ -49,26 +61,5 @@
     void SetIdentifier(const std::string& webServiceIdentifier);
 
     void SetUserProfileUrl(const std::string& url);
-
-    virtual bool IsGranted(unsigned int& validity,
-                           OrthancPluginHttpMethod method,
-                           const AccessedResource& access,
-                           const Token& token,
-                           const std::string& tokenValue)
-    {
-      return IsGrantedInternal(validity, method, access, &token, tokenValue);
-    }
-    
-    virtual bool IsGranted(unsigned int& validity,
-                           OrthancPluginHttpMethod method,
-                           const AccessedResource& access)
-    {
-      return IsGrantedInternal(validity, method, access, NULL, "");
-    }
-
-    virtual bool GetUserProfile(Json::Value& profile /* out */,
-                                const Token& token,
-                                const std::string& tokenValue);
-
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugin/BaseAuthorizationService.h	Wed Feb 22 13:13:38 2023 +0100
@@ -0,0 +1,111 @@
+/**
+ * Advanced authorization plugin for Orthanc
+ * Copyright (C) 2017-2023 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/>.
+ **/
+
+#pragma once
+
+#include "IAuthorizationService.h"
+
+
+namespace OrthancPlugins
+{
+  class CachedAuthorizationService;
+
+  class BaseAuthorizationService : public IAuthorizationService
+  {
+    friend CachedAuthorizationService;
+  protected:
+    virtual bool IsGrantedInternal(unsigned int& validity,
+                                   OrthancPluginHttpMethod method,
+                                   const AccessedResource& access,
+                                   const Token* token,
+                                   const std::string& tokenValue) = 0;
+    
+    virtual bool GetUserProfileInternal(unsigned int& validity,
+                                        Json::Value& profile /* out */,
+                                        const Token* token,
+                                        const std::string& tokenValue) = 0;
+
+    virtual bool HasUserPermissionInternal(unsigned int& validity,
+                                           const std::string& permission,
+                                           const Token* token,
+                                           const std::string& tokenValue) = 0;
+
+  public:
+    virtual ~BaseAuthorizationService()
+    {
+    }
+    
+    virtual bool IsGranted(unsigned int& validity,
+                           OrthancPluginHttpMethod method,
+                           const AccessedResource& access,
+                           const Token& token,
+                           const std::string& tokenValue)
+    {
+      return IsGrantedInternal(validity, method, access, &token, tokenValue);
+    }
+    
+    virtual bool IsGrantedToAnonymousUser(unsigned int& validity,
+                                          OrthancPluginHttpMethod method,
+                                          const AccessedResource& access)
+    {
+      return IsGrantedInternal(validity, method, access, NULL, "");
+    }
+
+    virtual bool GetUserProfile(unsigned int& validity,
+                                Json::Value& profile /* out */,
+                                const Token& token,
+                                const std::string& tokenValue)
+    {
+      return GetUserProfileInternal(validity, profile, &token, tokenValue);
+    }
+
+    virtual bool GetAnonymousUserProfile(unsigned int& validity /* out */,
+                                         Json::Value& profile /* out */)
+    {
+      return GetUserProfileInternal(validity, profile, NULL, "");
+    }
+
+    virtual bool HasUserPermission(unsigned int& validity /* out */,
+                                   const std::set<std::string>& anyOfPermissions,
+                                   const Token& token,
+                                   const std::string& tokenValue)
+    {
+      for (std::set<std::string>::const_iterator it = anyOfPermissions.begin(); it != anyOfPermissions.end(); ++it)
+      {
+        if (HasUserPermissionInternal(validity, *it, &token, tokenValue))
+        {
+          return true;
+        }
+      }
+      return false;
+    }
+
+    virtual bool HasAnonymousUserPermission(unsigned int& validity /* out */,
+                                            const std::set<std::string>& anyOfPermissions)
+    {
+      for (std::set<std::string>::const_iterator it = anyOfPermissions.begin(); it != anyOfPermissions.end(); ++it)
+      {
+        if (HasUserPermissionInternal(validity, *it, NULL, ""))
+        {
+          return true;
+        }
+      }
+      return false;
+    }
+  };
+}
--- a/Plugin/CachedAuthorizationService.cpp	Tue Feb 21 09:23:47 2023 +0100
+++ b/Plugin/CachedAuthorizationService.cpp	Wed Feb 22 13:13:38 2023 +0100
@@ -19,6 +19,7 @@
 #include "CachedAuthorizationService.h"
 
 #include <OrthancException.h>
+#include <Toolbox.h>
 
 #include <boost/lexical_cast.hpp>
 
@@ -35,7 +36,15 @@
   }
     
 
-  CachedAuthorizationService::CachedAuthorizationService(IAuthorizationService* decorated /* takes ownership */,
+  std::string CachedAuthorizationService::ComputeKey(const std::string& permission,
+                                                     const Token& token,
+                                                     const std::string& tokenValue) const
+  {
+    return (permission + "|" + token.GetKey() + "|" + tokenValue);
+  }
+
+
+  CachedAuthorizationService::CachedAuthorizationService(BaseAuthorizationService* decorated /* takes ownership */,
                                                          ICacheFactory& factory) :
     decorated_(decorated),
     cache_(factory.Create())
@@ -47,15 +56,15 @@
   }
 
 
-  bool CachedAuthorizationService::IsGranted(unsigned int& validity,
-                                             OrthancPluginHttpMethod method,
-                                             const AccessedResource& access,
-                                             const Token& token,
-                                             const std::string& tokenValue)
+  bool CachedAuthorizationService::IsGrantedInternal(unsigned int& validity,
+                                                     OrthancPluginHttpMethod method,
+                                                     const AccessedResource& access,
+                                                     const Token* token,
+                                                     const std::string& tokenValue)
   {
     assert(decorated_.get() != NULL);
 
-    std::string key = ComputeKey(method, access, token, tokenValue);
+    std::string key = ComputeKey(method, access, *token, tokenValue);
     std::string value;
 
     if (cache_->Retrieve(value, key))
@@ -64,7 +73,7 @@
       return (value == "1");
     }        
         
-    bool granted = decorated_->IsGranted(validity, method, access, token, tokenValue);
+    bool granted = decorated_->IsGrantedInternal(validity, method, access, token, tokenValue);
 
     if (granted)
     {
@@ -87,21 +96,53 @@
   }
 
   
-  bool CachedAuthorizationService::IsGranted(unsigned int& validity,
-                                             OrthancPluginHttpMethod method,
-                                             const AccessedResource& access)
+  bool CachedAuthorizationService::GetUserProfileInternal(unsigned int& validity,
+                                                          Json::Value& profile /* out */,
+                                                          const Token* token,
+                                                          const std::string& tokenValue)
+  {
+    // no cache used when retrieving the full user profile
+    return decorated_->GetUserProfileInternal(validity, profile, token, tokenValue);
+  }
+
+  bool CachedAuthorizationService::HasUserPermissionInternal(unsigned int& validity,
+                                                             const std::string& permission,
+                                                             const Token* token,
+                                                             const std::string& tokenValue)
   {
     assert(decorated_.get() != NULL);
 
-    // The cache is not used if no token is available
-    return decorated_->IsGranted(validity, method, access);
+    std::string key = ComputeKey(permission, *token, tokenValue);
+    std::string value;
+
+    if (cache_->Retrieve(value, key))
+    {
+      // Return the previously cached value
+      return (value == "1");
+    }        
+        
+    bool granted = decorated_->HasUserPermissionInternal(validity, permission, token, tokenValue);
+
+    if (granted)
+    {
+      if (validity > 0)
+      {
+        cache_->Store(key, "1", validity);
+      }
+        
+      return true;
+    }
+    else
+    {
+      if (validity > 0)
+      {
+        cache_->Store(key, "0", validity);
+      }
+        
+      return false;
+    }
   }
 
-  bool CachedAuthorizationService::GetUserProfile(Json::Value& profile /* out */,
-                                                  const Token& token,
-                                                  const std::string& tokenValue)
-  {
-    return decorated_->GetUserProfile(profile, token, tokenValue);
-  }
+
 
 }
--- a/Plugin/CachedAuthorizationService.h	Tue Feb 21 09:23:47 2023 +0100
+++ b/Plugin/CachedAuthorizationService.h	Wed Feb 22 13:13:38 2023 +0100
@@ -18,7 +18,7 @@
 
 #pragma once
 
-#include "IAuthorizationService.h"
+#include "BaseAuthorizationService.h"
 #include "ICacheFactory.h"
 
 #include <Compatibility.h>  // For std::unique_ptr<>
@@ -30,33 +30,41 @@
   /**
    * Decorator design pattern to add a cache around an IAuthorizationService
    **/
-  class CachedAuthorizationService : public IAuthorizationService
+  class CachedAuthorizationService : public BaseAuthorizationService
   {
   private:
-    std::unique_ptr<IAuthorizationService>  decorated_;
+    std::unique_ptr<BaseAuthorizationService>  decorated_;
     std::unique_ptr<ICache>   cache_;
 
     std::string ComputeKey(OrthancPluginHttpMethod method,
                            const AccessedResource& access,
                            const Token& token,
                            const std::string& tokenValue) const;
+
+    std::string ComputeKey(const std::string& permission,
+                           const Token& token,
+                           const std::string& tokenValue) const;
+
+    virtual bool IsGrantedInternal(unsigned int& validity,
+                                   OrthancPluginHttpMethod method,
+                                   const AccessedResource& access,
+                                   const Token* token,
+                                   const std::string& tokenValue) ORTHANC_OVERRIDE;
     
+    virtual bool GetUserProfileInternal(unsigned int& validity,
+                                        Json::Value& profile /* out */,
+                                        const Token* token,
+                                        const std::string& tokenValue) ORTHANC_OVERRIDE;
+
+    virtual bool HasUserPermissionInternal(unsigned int& validity,
+                                           const std::string& permission,
+                                           const Token* token,
+                                           const std::string& tokenValue) ORTHANC_OVERRIDE;
+
+
   public:
-    CachedAuthorizationService(IAuthorizationService* decorated /* takes ownership */,
+    CachedAuthorizationService(BaseAuthorizationService* decorated /* takes ownership */,
                                ICacheFactory& factory);
 
-    virtual bool IsGranted(unsigned int& validity,
-                           OrthancPluginHttpMethod method,
-                           const AccessedResource& access,
-                           const Token& token,
-                           const std::string& tokenValue) ORTHANC_OVERRIDE;
-    
-    virtual bool IsGranted(unsigned int& validity,
-                           OrthancPluginHttpMethod method,
-                           const AccessedResource& access) ORTHANC_OVERRIDE;
- 
-    virtual bool GetUserProfile(Json::Value& profile /* out */,
-                                const Token& token,
-                                const std::string& tokenValue);
  };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugin/DefaultConfiguration.json	Wed Feb 22 13:13:38 2023 +0100
@@ -0,0 +1,71 @@
+{
+    "Authorization" : {
+        // The URL of the auth webservice implementing resource level authorization (optional if not implementing resource based permissions)
+        // "WebService" : "http://change-me:8000/validate",
+        
+        // The URL of the auth webservice implementing resource level authorization (optional if not implementing user-permissions)
+        // "WebServiceUserProfileUrl" : "http://change-me:8000/user-profile",
+
+        // The username and password to connect to the webservice (optional)
+        //"WebServiceUsername": "change-me",
+        //"WebServicePassword": "change-me",
+        
+        // An identifier added to the payload of each request to the auth webservice (optional)
+        //"WebServiceIdentifier": "change-me"
+
+        // The name of the HTTP headers that may contain auth tokens
+        //"TokenHttpHeaders" : [],
+        
+        // the name of the GET arguments that may contain auth tokens
+        //"TokenGetArguments" : [],
+
+        // A list of predefined configurations for well-known plugins
+        // "StandardConfigurations": [               // new in v 0.4.0
+        //     "osimis-web-viewer",
+        //     "stone-webviewer",
+        //     "orthanc-explorer-2"
+        // ],
+
+        //"UncheckedResources" : [],
+        //"UncheckedFolders" : [],
+        //"CheckedLevel" : "studies",
+        //"UncheckedLevels" : [],
+
+        // Definition of required "user-permissions".  This can be fully customized.
+        // You may define other permissions yourself as long as they mathc the permissions
+        // provided in the user-profile route implemented by the auth-service.
+        // You may test your regex in https://regex101.com/ by selecting .NET (C#) and removing the leading ^ and trailing $
+        // The default configuration is suitable for Orthanc-Explorer-2 (see TBD sample)
+        "Permissions" : [
+            // elemental browsing in OE2
+            ["post", "^/tools/find$", "all|view"],
+            ["get" , "^/(patients|studies|series|instances)/([a-f0-9-]+)/(studies|series|instances)$", "all|view"],
+            ["get" , "^/instances/([a-f0-9-]+)/(tags|header)$", "all|view"],
+            ["get" , "^/statistics$", "all|view"],
+
+            // monitor jobs you have created
+            ["get" , "^/jobs/([a-f0-9-]+)$", "all|send|modify|anonymize|q-r-remote-modalities"],
+
+            // downloads: not functional yet, we need one-time-tokens
+            ["get" , "^/(patients|studies|series|instances)/([a-f0-9-]+)/archive$", "all|download"],
+            ["get" , "^/(patients|studies|series|instances)/([a-f0-9-]+)/media$", "all|download"],
+
+            // interacting with peers/modalities/dicomweb
+            ["post", "^/(peers|modalities)/(.*)/store$", "all|send"],
+            ["get" , "^/(peers|modalities)$", "all|send|q-r-remote-modalities"],
+            ["post", "^/modalities/(.*)/echo$", "all|send|q-r-remote-modalities"],
+            ["post", "^/modalities/(.*)/query$", "all|q-r-remote-modalities"],
+            ["get", "^/queries/([a-f0-9-]+)/answers$", "all|q-r-remote-modalities"],
+            ["post", "^/modalities/(.*)/move$", "all|q-r-remote-modalities"],
+            ["get" , "^/DICOM_WEB_ROOT/(servers)/(.*)/stow$", "all|send"],
+
+            // upload
+            ["post", "^/instances$", "all|upload"],
+
+            // modifications/anonymization
+            ["post", "^/(patients|studies|series|instances)/([a-f0-9-]+)/modify(.*)$", "all|modify"],
+            ["post", "^/(patients|studies|series|instances)/([a-f0-9-]+)/anonymize(.*)$", "all|anonymize"]
+
+        ]
+    }
+}
\ No newline at end of file
--- a/Plugin/Enumerations.cpp	Tue Feb 21 09:23:47 2023 +0100
+++ b/Plugin/Enumerations.cpp	Wed Feb 22 13:13:38 2023 +0100
@@ -79,7 +79,7 @@
     }
     else
     {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, std::string("Invalid access level: ") + tmp);
     }
   }
 }
--- a/Plugin/IAuthorizationService.h	Tue Feb 21 09:23:47 2023 +0100
+++ b/Plugin/IAuthorizationService.h	Wed Feb 22 13:13:38 2023 +0100
@@ -41,12 +41,24 @@
                            const Token& token,
                            const std::string& tokenValue) = 0;
     
-    virtual bool IsGranted(unsigned int& validity /* out */,
-                           OrthancPluginHttpMethod method,
-                           const AccessedResource& access) = 0;
+    virtual bool IsGrantedToAnonymousUser(unsigned int& validity /* out */,
+                                          OrthancPluginHttpMethod method,
+                                          const AccessedResource& access) = 0;
 
-    virtual bool GetUserProfile(Json::Value& profile /* out */,
+    virtual bool GetUserProfile(unsigned int& validity /* out */,
+                                Json::Value& profile /* out */,
                                 const Token& token,
                                 const std::string& tokenValue) = 0;
+
+    virtual bool GetAnonymousUserProfile(unsigned int& validity /* out */,
+                                         Json::Value& profile /* out */) = 0;
+
+    virtual bool HasUserPermission(unsigned int& validity /* out */,
+                                   const std::set<std::string>& anyOfPermissions,
+                                   const Token& token,
+                                   const std::string& tokenValue) = 0;
+
+    virtual bool HasAnonymousUserPermission(unsigned int& validity /* out */,
+                                            const std::set<std::string>& anyOfPermissions) = 0;
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugin/PermissionParser.cpp	Wed Feb 22 13:13:38 2023 +0100
@@ -0,0 +1,161 @@
+/**
+ * Advanced authorization plugin for Orthanc
+ * Copyright (C) 2017-2023 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 "PermissionParser.h"
+
+#include <Toolbox.h>
+#include <OrthancException.h>
+#include <Logging.h>
+
+namespace OrthancPlugins
+{
+  PermissionPattern::PermissionPattern(const OrthancPluginHttpMethod& method, const std::string& patternRegex, const std::string& permissions) :
+    method(method),
+    pattern(patternRegex)
+  {
+    std::vector<std::string> permissionsVector;
+    Orthanc::Toolbox::TokenizeString(permissionsVector, permissions, '|');
+
+    for (size_t i = 0; i < permissionsVector.size(); ++i)
+    {
+      this->permissions.insert(permissionsVector[i]);
+    }
+  }
+
+
+  static void Replace(std::string& text, const std::string& findText, const std::string& replaceText)
+  {
+    size_t pos = text.find(findText);
+    if (pos != std::string::npos)
+    {
+      text = text.replace(pos, findText.size(), replaceText);
+    }
+  }
+
+
+  static void StripLeadingAndTrailingSlashes(std::string& text)
+  {
+    if (text.size() > 1 && text[0] == '/')
+    {
+      text = text.substr(1, text.size() -1);
+    }
+    if (text.size() > 1 && text[text.size() - 1] == '/')
+    {
+      text = text.substr(0, text.size() -1);
+    }
+  }
+
+
+  PermissionParser::PermissionParser(const std::string& dicomWebRoot, const std::string& oe2Root) :
+    dicomWebRoot_(dicomWebRoot),
+    oe2Root_(oe2Root)
+  {
+  }
+
+  void PermissionParser::Add(const Json::Value& configuration)
+  {
+    if (configuration.type() != Json::arrayValue)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadParameterType, "Permissions should be an array.");
+    }
+
+    for (Json::ArrayIndex i = 0; i < configuration.size(); ++i)
+    {
+      const Json::Value& permission = configuration[i];
+      if (permission.type() != Json::arrayValue || permission.size() < 3)
+      {
+        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
+      );
+    }
+
+  }
+
+  void PermissionParser::Add(const std::string& method,
+                             const std::string& patternRegex,
+                             const std::string& permission)
+  {
+    std::string lowerCaseMethod;
+    Orthanc::Toolbox::ToLowerCase(lowerCaseMethod, method);
+    OrthancPluginHttpMethod parsedMethod = OrthancPluginHttpMethod_Get;
+
+    if (lowerCaseMethod == "post")
+    {
+      parsedMethod = OrthancPluginHttpMethod_Post;
+    }
+    else if (lowerCaseMethod == "put")
+    {
+      parsedMethod = OrthancPluginHttpMethod_Put;
+    }
+    else if (lowerCaseMethod == "delete")
+    {
+      parsedMethod = OrthancPluginHttpMethod_Delete;
+    }
+    else if (lowerCaseMethod == "get")
+    {
+      parsedMethod = OrthancPluginHttpMethod_Get;
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, std::string("Invalid HTTP method ") + method);
+    }
+
+    std::string regex = patternRegex;
+    std::string strippedDicomWebRoot = dicomWebRoot_;
+
+    StripLeadingAndTrailingSlashes(strippedDicomWebRoot);
+    Replace(regex, "DICOM_WEB_ROOT", strippedDicomWebRoot);
+
+    LOG(WARNING) << "Authorization plugin: adding a new permission pattern: " << lowerCaseMethod << " " << regex << " - " << permission;
+
+    permissionsPattern_.push_back(PermissionPattern(parsedMethod, regex, permission));
+  }
+
+  bool PermissionParser::Parse(std::set<std::string>& permissions,
+                               std::string& matchedPattern,
+                               const OrthancPluginHttpMethod& method,
+                               const std::string& uri) const
+  {
+    // The mutex below should not be necessary, but we prefer to
+    // ensure thread safety in boost::regex
+    boost::mutex::scoped_lock lock(mutex_);
+
+
+    for (std::list<PermissionPattern>::const_iterator it = permissionsPattern_.begin();
+      it != permissionsPattern_.end(); ++it)
+    {
+      if (method == it->method)
+      {
+        boost::smatch what;
+        if (boost::regex_match(uri, what, it->pattern))
+        {
+          matchedPattern = it->pattern.expression();
+          permissions = it->permissions;
+          return true;
+        }
+      }
+    }
+
+    return false;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugin/PermissionParser.h	Wed Feb 22 13:13:38 2023 +0100
@@ -0,0 +1,60 @@
+/**
+ * Advanced authorization plugin for Orthanc
+ * Copyright (C) 2017-2023 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/>.
+ **/
+
+#pragma once
+
+#include "AuthorizationParserBase.h"
+
+#include <boost/regex.hpp>
+#include <boost/thread/mutex.hpp>
+
+namespace OrthancPlugins
+{
+  struct PermissionPattern
+  {
+    OrthancPluginHttpMethod   method;
+    boost::regex              pattern;
+    std::set<std::string>     permissions;
+
+    PermissionPattern(const OrthancPluginHttpMethod& method, const std::string& patternRegex, const std::string& permissions);
+  };
+
+  class PermissionParser
+  { 
+  private:
+    mutable boost::mutex mutex_; 
+    std::list<PermissionPattern> permissionsPattern_;
+    std::string dicomWebRoot_;
+    std::string oe2Root_;
+
+  public:
+    PermissionParser(const std::string& dicomWebRoot,
+                     const std::string& oe2Root);
+
+    void Add(const std::string& method,
+             const std::string& patternRegex,
+             const std::string& permission);
+
+    void Add(const Json::Value& configuration);
+
+    bool Parse(std::set<std::string>& permissions,
+               std::string& matchedPattern,
+               const OrthancPluginHttpMethod& method,
+               const std::string& uri) const;
+  };
+}
--- a/Plugin/Plugin.cpp	Tue Feb 21 09:23:47 2023 +0100
+++ b/Plugin/Plugin.cpp	Wed Feb 22 13:13:38 2023 +0100
@@ -20,6 +20,7 @@
 #include "DefaultAuthorizationParser.h"
 #include "CachedAuthorizationService.h"
 #include "AuthorizationWebService.h"
+#include "PermissionParser.h"
 #include "MemoryCache.h"
 
 #include "../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h"
@@ -27,17 +28,27 @@
 #include <Compatibility.h>  // For std::unique_ptr<>
 #include <Logging.h>
 #include <Toolbox.h>
+#include <EmbeddedResources.h>
 
 
 // Configuration of the authorization plugin
 static std::unique_ptr<OrthancPlugins::IAuthorizationParser> authorizationParser_;
 static std::unique_ptr<OrthancPlugins::IAuthorizationService> authorizationService_;
+static std::unique_ptr<OrthancPlugins::PermissionParser> permissionParser_;
 static std::set<std::string> uncheckedResources_;
 static std::list<std::string> uncheckedFolders_;
 static std::set<OrthancPlugins::Token> tokens_;
 static std::set<OrthancPlugins::AccessLevel> uncheckedLevels_;
 
 
+static std::string JoinStrings(const std::set<std::string>& values)
+{
+  std::string out;
+  std::set<std::string> copy = values;    // TODO: remove after upgrading to OrthancFramework 1.11.3+
+  Orthanc::Toolbox::JoinStrings(out, copy, "|");
+  return out;
+}
+
 static int32_t FilterHttpRequests(OrthancPluginHttpMethod method,
                                   const char *uri,
                                   const char *ip,
@@ -68,6 +79,52 @@
       }
     }
 
+    unsigned int validity;  // ignored
+
+    // check if the user permissions grants him access
+    if (permissionParser_.get() != NULL &&
+      authorizationService_.get() != NULL) 
+      // && uncheckedLevels_.find(OrthancPlugins::AccessLevel_UserPermissions) == uncheckedLevels_.end())
+    {
+      std::set<std::string> requiredPermissions;
+      std::string matchedPattern;
+      if (permissionParser_->Parse(requiredPermissions, matchedPattern, method, uri))
+      {
+        if (tokens_.empty())
+        {
+          LOG(INFO) << "Testing whether anonymous user has any of the required permissions '" << JoinStrings(requiredPermissions) << "'";
+          if (authorizationService_->HasAnonymousUserPermission(validity, requiredPermissions))
+          {
+            return 1;
+          }
+        }
+        else
+        {
+          OrthancPlugins::AssociativeArray headers
+            (headersCount, headersKeys, headersValues, false);
+
+          // Loop over all the authorization tokens stored in the HTTP
+          // headers, until finding one that is granted
+          for (std::set<OrthancPlugins::Token>::const_iterator
+                  token = tokens_.begin(); token != tokens_.end(); ++token)
+          {
+            std::string value;
+
+            // we consider that users only works with HTTP Header tokens, not tokens from GetArgument
+            if (token->GetType() == OrthancPlugins::TokenType_HttpHeader &&
+              headers.GetValue(value, token->GetKey()))
+            {
+              LOG(INFO) << "Testing whether user has the required permission '" << JoinStrings(requiredPermissions) << "' based on the '" << token->GetKey() << "' HTTP header required to match '" << matchedPattern << "'";
+              if (authorizationService_->HasUserPermission(validity, requiredPermissions, *token, value))
+              {
+                return 1;
+              }
+            }
+          }
+        }
+      }
+    }
+
     if (authorizationParser_.get() != NULL &&
         authorizationService_.get() != NULL)
     {
@@ -94,11 +151,10 @@
                     << " \"" << access->GetOrthancId() << "\" is allowed";
 
           bool granted = false;
-          unsigned int validity;  // ignored
 
           if (tokens_.empty())
           {
-            granted = authorizationService_->IsGranted(validity, method, *access);
+            granted = authorizationService_->IsGrantedToAnonymousUser(validity, method, *access);
           }
           else
           {
@@ -193,7 +249,7 @@
   {
     if (authorizationParser_.get() == NULL)
     {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      return OrthancPluginErrorCode_Success;
     }
     
     if (changeType == OrthancPluginChangeType_Deleted)
@@ -285,7 +341,8 @@
       
       if (hasValue)
       {
-        authorizationService_->GetUserProfile(profile, *token, value);
+        unsigned int validity; // not used
+        authorizationService_->GetUserProfile(validity, profile, *token, value);
         
         OrthancPlugins::AnswerJson(profile, output);
         break;
@@ -295,6 +352,30 @@
   }
 }
 
+void MergeJson(Json::Value &a, const Json::Value &b) {                                                                        
+                                                                                                                  
+  if (!a.isObject() || !b.isObject())
+  {
+    return;
+  }
+
+  Json::Value::Members members = b.getMemberNames();
+
+  for (size_t i = 0; i < members.size(); i++)
+  {
+    std::string key = members[i];
+    
+    if (!a[key].isNull() && a[key].type() == Json::objectValue && b[key].type() == Json::objectValue)
+    {
+      MergeJson(a[key], b[key]);
+    } 
+    else
+    {
+      a[key] = b[key];
+    }
+  }
+}
+
 
 extern "C"
 {
@@ -322,46 +403,60 @@
 
     try
     {
-      OrthancPlugins::OrthancConfiguration general;
+      static const char* PLUGIN_SECTION = "Authorization";
+
+      OrthancPlugins::OrthancConfiguration orthancFullConfiguration;
+
+      // read default configuration
+      std::string defaultConfigurationFileContent;
+      Orthanc::EmbeddedResources::GetFileResource(defaultConfigurationFileContent, Orthanc::EmbeddedResources::DEFAULT_CONFIGURATION);
+      Json::Value pluginJsonDefaultConfiguration;
+      OrthancPlugins::ReadJsonWithoutComments(pluginJsonDefaultConfiguration, defaultConfigurationFileContent);
+      Json::Value pluginJsonConfiguration = pluginJsonDefaultConfiguration[PLUGIN_SECTION];
 
-      static const char* SECTION = "Authorization";
-      if (general.IsSection(SECTION))
+      OrthancPlugins::OrthancConfiguration pluginProvidedConfiguration;
+
+      if (orthancFullConfiguration.IsSection(PLUGIN_SECTION))
       {
-        OrthancPlugins::OrthancConfiguration configuration;
-        general.GetSection(configuration, "Authorization");
+        // get the configuration provided by the user
+        orthancFullConfiguration.GetSection(pluginProvidedConfiguration, PLUGIN_SECTION);
+
+        // merge it with the default configuration.  This is a way to apply the all default values in a single step
+        MergeJson(pluginJsonConfiguration, pluginProvidedConfiguration.GetJson());
+
+        // recreate a OrthancConfiguration object from the merged configuration
+        OrthancPlugins::OrthancConfiguration pluginConfiguration(pluginJsonConfiguration, PLUGIN_SECTION);
 
         // TODO - The size of the caches is set to 10,000 items. Maybe add a configuration option?
         OrthancPlugins::MemoryCache::Factory factory(10000);
 
-        {
-          std::string root;
+        std::string dicomWebRoot = "/dicom-web/";
+        std::string oe2Root = "/ui/";
 
-          if (configuration.IsSection("DicomWeb"))
-          {
-            OrthancPlugins::OrthancConfiguration dicomWeb;
-            dicomWeb.GetSection(configuration, "DicomWeb");
-            root = dicomWeb.GetStringValue("Root", "");
-          }
+        if (orthancFullConfiguration.IsSection("DicomWeb"))
+        {
+          OrthancPlugins::OrthancConfiguration dicomWeb;
+          dicomWeb.GetSection(orthancFullConfiguration, "DicomWeb");
+          dicomWebRoot = dicomWeb.GetStringValue("Root", "/dicom-web/");
+        }
 
-          if (root.empty())
-          {
-            root = "/dicom-web/";
-          } 
-
-          authorizationParser_.reset
-            (new OrthancPlugins::DefaultAuthorizationParser(factory, root));
+        if (orthancFullConfiguration.IsSection("OrthancExplorer2"))
+        {
+          OrthancPlugins::OrthancConfiguration oe2;
+          oe2.GetSection(orthancFullConfiguration, "OrthancExplorer2");
+          oe2Root = oe2.GetStringValue("Root", "/ui/");
         }
 
         std::list<std::string> tmp;
 
-        configuration.LookupListOfStrings(tmp, "TokenHttpHeaders", true);
+        pluginConfiguration.LookupListOfStrings(tmp, "TokenHttpHeaders", true);
         for (std::list<std::string>::const_iterator
                it = tmp.begin(); it != tmp.end(); ++it)
         {
           tokens_.insert(OrthancPlugins::Token(OrthancPlugins::TokenType_HttpHeader, *it));
         }
 
-        configuration.LookupListOfStrings(tmp, "TokenGetArguments", true);
+        pluginConfiguration.LookupListOfStrings(tmp, "TokenGetArguments", true);
 
 #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 3, 0)  
         for (std::list<std::string>::const_iterator
@@ -379,22 +474,49 @@
         }
 #endif
 
-        configuration.LookupSetOfStrings(uncheckedResources_, "UncheckedResources", false);
-        configuration.LookupListOfStrings(uncheckedFolders_, "UncheckedFolders", false);
+        pluginConfiguration.LookupSetOfStrings(uncheckedResources_, "UncheckedResources", false);
+        pluginConfiguration.LookupListOfStrings(uncheckedFolders_, "UncheckedFolders", false);
 
         std::string url;
 
         static const char* WEB_SERVICE = "WebService";
-        if (!configuration.LookupStringValue(url, WEB_SERVICE))
+        if (!pluginConfiguration.LookupStringValue(url, WEB_SERVICE))
+        {
+          LOG(WARNING) << "Authorization plugin: no \"" << WEB_SERVICE << "\" configuration provided.  Will not perform resource based authorization.";
+        }
+        else
+        {
+          authorizationParser_.reset
+            (new OrthancPlugins::DefaultAuthorizationParser(factory, dicomWebRoot));
+        }
+
+        static const char* WEB_SERVICE_USER_PROFILE = "WebServiceUserProfileUrl";
+        static const char* PERMISSIONS = "Permissions";        
+        if (!pluginConfiguration.LookupStringValue(url, WEB_SERVICE_USER_PROFILE))
         {
-          throw Orthanc::OrthancException(
-            Orthanc::ErrorCode_BadFileFormat,
-            "Missing mandatory option \"" + std::string(WEB_SERVICE) +
-            "\" for the authorization plugin");
+          LOG(WARNING) << "Authorization plugin: no \"" << WEB_SERVICE_USER_PROFILE << "\" configuration provided.  Will not perform user-permissions based authorization.";
+        }
+        else
+        {
+          if (!pluginConfiguration.GetJson().isMember(PERMISSIONS))
+          {
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "Authorization plugin: Missing required \"" + std::string(PERMISSIONS) + 
+              "\" option since you have defined the \"" + std::string(WEB_SERVICE_USER_PROFILE) + "\" option");
+          }
+          permissionParser_.reset
+            (new OrthancPlugins::PermissionParser(dicomWebRoot, oe2Root));
+
+          permissionParser_->Add(pluginConfiguration.GetJson()[PERMISSIONS]);
+        }
+
+        if (authorizationParser_.get() == NULL && permissionParser_.get() == NULL)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "Authorization plugin: Missing one of the mandatory option \"" + std::string(WEB_SERVICE) +
+            "\" or \"" + std::string(WEB_SERVICE_USER_PROFILE) + "\"");
         }
 
         std::set<std::string> standardConfigurations;
-        if (configuration.LookupSetOfStrings(standardConfigurations, "StandardConfigurations", false))
+        if (pluginConfiguration.LookupSetOfStrings(standardConfigurations, "StandardConfigurations", false))
         {
           if (standardConfigurations.find("osimis-web-viewer") != standardConfigurations.end())
           {
@@ -419,6 +541,7 @@
           {
             uncheckedFolders_.push_back("/ui/app/");
             uncheckedResources_.insert("/ui/api/pre-login-configuration");        // for the UI to know, i.e. if Keycloak is enabled or not
+            uncheckedResources_.insert("/ui/api/configuration");
             uncheckedResources_.insert("/auth/user-profile");
 
             tokens_.insert(OrthancPlugins::Token(OrthancPlugins::TokenType_HttpHeader, "Authorization"));  // for basic-auth
@@ -428,7 +551,7 @@
         }
 
         std::string checkedLevelString;
-        if (configuration.LookupStringValue(checkedLevelString, "CheckedLevel"))
+        if (pluginConfiguration.LookupStringValue(checkedLevelString, "CheckedLevel"))
         {
           OrthancPlugins::AccessLevel checkedLevel = OrthancPlugins::StringToAccessLevel(checkedLevelString);
           if (checkedLevel == OrthancPlugins::AccessLevel_Instance) 
@@ -457,7 +580,7 @@
           }
         }
 
-        if (configuration.LookupListOfStrings(tmp, "UncheckedLevels", false))
+        if (pluginConfiguration.LookupListOfStrings(tmp, "UncheckedLevels", false))
         {
           if (uncheckedLevels_.size() == 0)
           {
@@ -477,20 +600,20 @@
         std::unique_ptr<OrthancPlugins::AuthorizationWebService> webService(new OrthancPlugins::AuthorizationWebService(url));
 
         std::string webServiceIdentifier;
-        if (configuration.LookupStringValue(webServiceIdentifier, "WebServiceIdentifier"))
+        if (pluginConfiguration.LookupStringValue(webServiceIdentifier, "WebServiceIdentifier"))
         {
           webService->SetIdentifier(webServiceIdentifier);
         }
 
         std::string webServiceUsername;
         std::string webServicePassword;
-        if (configuration.LookupStringValue(webServiceUsername, "WebServiceUsername") && configuration.LookupStringValue(webServicePassword, "WebServicePassword"))
+        if (pluginConfiguration.LookupStringValue(webServiceUsername, "WebServiceUsername") && pluginConfiguration.LookupStringValue(webServicePassword, "WebServicePassword"))
         {
           webService->SetCredentials(webServiceUsername, webServicePassword);
         }
 
         std::string webServiceUserProfileUrl;
-        if (configuration.LookupStringValue(webServiceUserProfileUrl, "WebServiceUserProfileUrl"))
+        if (pluginConfiguration.LookupStringValue(webServiceUserProfileUrl, "WebServiceUserProfileUrl"))
         {
           webService->SetUserProfileUrl(webServiceUserProfileUrl);
         }
@@ -510,7 +633,7 @@
       }
       else
       {
-        LOG(WARNING) << "No section \"" << SECTION << "\" in the configuration file, "
+        LOG(WARNING) << "No section \"" << PLUGIN_SECTION << "\" in the configuration file, "
                      << "the authorization plugin is disabled";
       }
     }
--- a/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp	Tue Feb 21 09:23:47 2023 +0100
+++ b/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp	Wed Feb 22 13:13:38 2023 +0100
@@ -750,6 +750,12 @@
     }
   }
 
+  OrthancConfiguration::OrthancConfiguration(const Json::Value& configuration, const std::string& path) :
+    configuration_(configuration),
+    path_(path)
+  {
+  }
+
 
   std::string OrthancConfiguration::GetPath(const std::string& key) const
   {
@@ -1105,7 +1111,7 @@
     if (configuration_[key].type() != Json::objectValue)
     {
       LogError("The configuration option \"" + GetPath(key) +
-               "\" is not a string as expected");
+               "\" is not an object as expected");
 
       ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
     }
--- a/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h	Tue Feb 21 09:23:47 2023 +0100
+++ b/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h	Wed Feb 22 13:13:38 2023 +0100
@@ -346,10 +346,12 @@
     void LoadConfiguration();
     
   public:
-    OrthancConfiguration();
+    OrthancConfiguration(); // loads the full Orthanc configuration
 
     explicit OrthancConfiguration(bool load);
 
+    explicit OrthancConfiguration(const Json::Value& configuration, const std::string& path);  // e.g. to load a section from a default json content
+
     const Json::Value& GetJson() const
     {
       return configuration_;