# HG changeset patch # User Alain Mazy # Date 1677068018 -3600 # Node ID 30fb3ce960d9094b6d8e4ddd42209ca9be063664 # Parent 786b202ef24ef543ce6622ab173649715620dfdc configurable user permissions diff -r 786b202ef24e -r 30fb3ce960d9 CMakeLists.txt --- 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 diff -r 786b202ef24e -r 30fb3ce960d9 NEWS --- 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 ==================== diff -r 786b202ef24e -r 30fb3ce960d9 Plugin/AuthorizationWebService.cpp --- 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 #include #include +#include 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; + } + } diff -r 786b202ef24e -r 30fb3ce960d9 Plugin/AuthorizationWebService.h --- 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 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); - }; } diff -r 786b202ef24e -r 30fb3ce960d9 Plugin/BaseAuthorizationService.h --- /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 . + **/ + +#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& anyOfPermissions, + const Token& token, + const std::string& tokenValue) + { + for (std::set::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& anyOfPermissions) + { + for (std::set::const_iterator it = anyOfPermissions.begin(); it != anyOfPermissions.end(); ++it) + { + if (HasUserPermissionInternal(validity, *it, NULL, "")) + { + return true; + } + } + return false; + } + }; +} diff -r 786b202ef24e -r 30fb3ce960d9 Plugin/CachedAuthorizationService.cpp --- 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 +#include #include @@ -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); - } + } diff -r 786b202ef24e -r 30fb3ce960d9 Plugin/CachedAuthorizationService.h --- 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 // 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 decorated_; + std::unique_ptr decorated_; std::unique_ptr 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); }; } diff -r 786b202ef24e -r 30fb3ce960d9 Plugin/DefaultConfiguration.json --- /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 diff -r 786b202ef24e -r 30fb3ce960d9 Plugin/Enumerations.cpp --- 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); } } } diff -r 786b202ef24e -r 30fb3ce960d9 Plugin/IAuthorizationService.h --- 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& anyOfPermissions, + const Token& token, + const std::string& tokenValue) = 0; + + virtual bool HasAnonymousUserPermission(unsigned int& validity /* out */, + const std::set& anyOfPermissions) = 0; }; } diff -r 786b202ef24e -r 30fb3ce960d9 Plugin/PermissionParser.cpp --- /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 . + **/ + +#include "PermissionParser.h" + +#include +#include +#include + +namespace OrthancPlugins +{ + PermissionPattern::PermissionPattern(const OrthancPluginHttpMethod& method, const std::string& patternRegex, const std::string& permissions) : + method(method), + pattern(patternRegex) + { + std::vector 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& 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::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; + } +} diff -r 786b202ef24e -r 30fb3ce960d9 Plugin/PermissionParser.h --- /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 . + **/ + +#pragma once + +#include "AuthorizationParserBase.h" + +#include +#include + +namespace OrthancPlugins +{ + struct PermissionPattern + { + OrthancPluginHttpMethod method; + boost::regex pattern; + std::set permissions; + + PermissionPattern(const OrthancPluginHttpMethod& method, const std::string& patternRegex, const std::string& permissions); + }; + + class PermissionParser + { + private: + mutable boost::mutex mutex_; + std::list 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& permissions, + std::string& matchedPattern, + const OrthancPluginHttpMethod& method, + const std::string& uri) const; + }; +} diff -r 786b202ef24e -r 30fb3ce960d9 Plugin/Plugin.cpp --- 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 // For std::unique_ptr<> #include #include +#include // Configuration of the authorization plugin static std::unique_ptr authorizationParser_; static std::unique_ptr authorizationService_; +static std::unique_ptr permissionParser_; static std::set uncheckedResources_; static std::list uncheckedFolders_; static std::set tokens_; static std::set uncheckedLevels_; +static std::string JoinStrings(const std::set& values) +{ + std::string out; + std::set 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 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::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 tmp; - configuration.LookupListOfStrings(tmp, "TokenHttpHeaders", true); + pluginConfiguration.LookupListOfStrings(tmp, "TokenHttpHeaders", true); for (std::list::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::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 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 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"; } } diff -r 786b202ef24e -r 30fb3ce960d9 Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp --- 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); } diff -r 786b202ef24e -r 30fb3ce960d9 Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h --- 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_;