# HG changeset patch # User Alain Mazy # Date 1677258816 -3600 # Node ID e381ba725669517e465d088f7d9e6e7fba2a4fcb # Parent 30fb3ce960d9094b6d8e4ddd42209ca9be063664 new PUT auth/tokens/{token-type} API route + updated interface with WebService diff -r 30fb3ce960d9 -r e381ba725669 NEWS --- a/NEWS Wed Feb 22 13:13:38 2023 +0100 +++ b/NEWS Fri Feb 24 18:13:36 2023 +0100 @@ -1,7 +1,12 @@ -* new "orthanc-explorer-2" StandardConfigurations -* new "auth/user-profile" Rest API route +* BREAKING-CHANGE: the API between the authorization plugin and the + WebService has slightly changed. Check the samples in the README (TODO). + - "identifier" has been renamed into "server-id" * new user-permission based authorization model. This is enabled if you define the new "WebServiceUserProfileUrl" configuration. +* new "orthanc-explorer-2" StandardConfigurations +* new GET "auth/user/profile" Rest API route to retrieve user permissions +* new PUT "auth/tokens/{token-type}" Rest API route to create tokens + 2022-11-16 - v 0.4.1 ==================== diff -r 30fb3ce960d9 -r e381ba725669 Plugin/AuthorizationWebService.cpp --- a/Plugin/AuthorizationWebService.cpp Wed Feb 22 13:13:38 2023 +0100 +++ b/Plugin/AuthorizationWebService.cpp Fri Feb 24 18:13:36 2023 +0100 @@ -82,15 +82,14 @@ if (!identifier_.empty()) { - body["identifier"] = identifier_; + body["server-id"] = identifier_; } else { - body["identifier"] = Json::nullValue; + body["server-id"] = Json::nullValue; } Orthanc::WebServiceParameters authWebservice; - authWebservice.SetUrl(url_); if (!username_.empty()) { @@ -101,6 +100,7 @@ Orthanc::Toolbox::WriteFastJson(bodyAsString, body); Orthanc::HttpClient authClient(authWebservice, ""); + authClient.SetUrl(tokenValidationUrl_); authClient.AssignBody(bodyAsString); authClient.SetMethod(Orthanc::HttpMethod_Post); authClient.AddHeader("Content-Type", "application/json"); @@ -158,16 +158,96 @@ password_ = password; } - void AuthorizationWebService::SetUserProfileUrl(const std::string& url) - { - userProfileUrl_ = url; - } - void AuthorizationWebService::SetIdentifier(const std::string& webServiceIdentifier) { identifier_ = webServiceIdentifier; } + bool AuthorizationWebService::CreateToken(IAuthorizationService::CreatedToken& response, + const std::string& tokenType, + const std::string& id, + const std::vector& resources, + const std::string& expirationDateString) + { + if (tokenCreationBaseUrl_.empty()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest, "Can not create tokens if the 'WebServiceTokenCreationBaseUrl' is not configured"); + } + std::string url = Orthanc::Toolbox::JoinUri(tokenCreationBaseUrl_, tokenType); + + Orthanc::WebServiceParameters authWebservice; + + if (!username_.empty()) + { + authWebservice.SetCredentials(username_, password_); + } + + Json::Value body; + + if (!id.empty()) + { + body["id"] = id; + } + + body["resources"] = Json::arrayValue; + for (size_t i = 0; i < resources.size(); ++i) + { + Json::Value resource; + if (!resources[i].dicomUid.empty()) + { + resource["dicom-uid"] = resources[i].dicomUid; + } + if (!resources[i].orthancId.empty()) + { + resource["orthanc-id"] = resources[i].orthancId; + } + if (!resources[i].url.empty()) + { + resource["url"] = resources[i].url; + } + if (!resources[i].level.empty()) + { + resource["level"] = resources[i].level; + } + + body["resources"].append(resource); + } + + body["type"] = tokenType; + if (!expirationDateString.empty()) + { + body["expiration-date"] = expirationDateString; + } + + std::string bodyAsString; + Orthanc::Toolbox::WriteFastJson(bodyAsString, body); + + Json::Value tokenResponse; + try + { + Orthanc::HttpClient authClient(authWebservice, ""); + authClient.SetUrl(url); + authClient.AssignBody(bodyAsString); + authClient.SetMethod(Orthanc::HttpMethod_Put); + authClient.AddHeader("Content-Type", "application/json"); + authClient.AddHeader("Expect", ""); + authClient.SetTimeout(10); + + authClient.ApplyAndThrowException(tokenResponse); + + response.token = tokenResponse["token"].asString(); + response.url = tokenResponse["url"].asString(); + + return true; + } + catch (Orthanc::OrthancException& ex) + { + return false; + } + + } + + bool AuthorizationWebService::GetUserProfileInternal(unsigned int& validity, Json::Value& profile /* out */, const Token* token, @@ -179,7 +259,6 @@ } Orthanc::WebServiceParameters authWebservice; - authWebservice.SetUrl(userProfileUrl_); if (!username_.empty()) { @@ -209,6 +288,7 @@ try { Orthanc::HttpClient authClient(authWebservice, ""); + authClient.SetUrl(userProfileUrl_); authClient.AssignBody(bodyAsString); authClient.SetMethod(Orthanc::HttpMethod_Post); authClient.AddHeader("Content-Type", "application/json"); diff -r 30fb3ce960d9 -r e381ba725669 Plugin/AuthorizationWebService.h --- a/Plugin/AuthorizationWebService.h Wed Feb 22 13:13:38 2023 +0100 +++ b/Plugin/AuthorizationWebService.h Fri Feb 24 18:13:36 2023 +0100 @@ -26,11 +26,12 @@ class AuthorizationWebService : public BaseAuthorizationService { private: - std::string url_; std::string username_; std::string password_; std::string identifier_; std::string userProfileUrl_; + std::string tokenValidationUrl_; + std::string tokenCreationBaseUrl_; protected: virtual bool IsGrantedInternal(unsigned int& validity, @@ -50,8 +51,12 @@ const std::string& tokenValue) ORTHANC_OVERRIDE; public: - AuthorizationWebService(const std::string& url) : - url_(url) + AuthorizationWebService(const std::string& tokenValidationUrl, + const std::string& tokenCreationBaseUrl, + const std::string& userProfileUrl) : + userProfileUrl_(userProfileUrl), + tokenValidationUrl_(tokenValidationUrl), + tokenCreationBaseUrl_(tokenCreationBaseUrl) { } @@ -60,6 +65,26 @@ void SetIdentifier(const std::string& webServiceIdentifier); - void SetUserProfileUrl(const std::string& url); + virtual bool HasUserProfile() const + { + return !userProfileUrl_.empty(); + } + + virtual bool HasCreateToken() const + { + return !tokenCreationBaseUrl_.empty(); + } + + virtual bool HasTokenValidation() const + { + return !tokenValidationUrl_.empty(); + } + + virtual bool CreateToken(IAuthorizationService::CreatedToken& response, + const std::string& tokenType, + const std::string& id, + const std::vector& resources, + const std::string& expirationDateString) ORTHANC_OVERRIDE; + }; } diff -r 30fb3ce960d9 -r e381ba725669 Plugin/CachedAuthorizationService.cpp --- a/Plugin/CachedAuthorizationService.cpp Wed Feb 22 13:13:38 2023 +0100 +++ b/Plugin/CachedAuthorizationService.cpp Fri Feb 24 18:13:36 2023 +0100 @@ -27,20 +27,36 @@ { std::string CachedAuthorizationService::ComputeKey(OrthancPluginHttpMethod method, const AccessedResource& access, - const Token& token, + const Token* token, const std::string& tokenValue) const { - return (boost::lexical_cast(method) + "|" + - boost::lexical_cast(access.GetLevel()) + "|" + - access.GetOrthancId() + "|" + token.GetKey() + "|" + tokenValue); + if (token != NULL) + { + return (boost::lexical_cast(method) + "|" + + boost::lexical_cast(access.GetLevel()) + "|" + + access.GetOrthancId() + "|" + token->GetKey() + "|" + tokenValue); + } + else + { + return (boost::lexical_cast(method) + "|" + + boost::lexical_cast(access.GetLevel()) + "|" + + access.GetOrthancId() + "|anonymous"); + } } std::string CachedAuthorizationService::ComputeKey(const std::string& permission, - const Token& token, + const Token* token, const std::string& tokenValue) const { - return (permission + "|" + token.GetKey() + "|" + tokenValue); + if (token != NULL) + { + return (permission + "|" + token->GetKey() + "|" + tokenValue); + } + else + { + return (permission + "|anonymous"); + } } @@ -64,7 +80,7 @@ { 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)) @@ -112,7 +128,7 @@ { assert(decorated_.get() != NULL); - std::string key = ComputeKey(permission, *token, tokenValue); + std::string key = ComputeKey(permission, token, tokenValue); std::string value; if (cache_->Retrieve(value, key)) diff -r 30fb3ce960d9 -r e381ba725669 Plugin/CachedAuthorizationService.h --- a/Plugin/CachedAuthorizationService.h Wed Feb 22 13:13:38 2023 +0100 +++ b/Plugin/CachedAuthorizationService.h Fri Feb 24 18:13:36 2023 +0100 @@ -38,11 +38,11 @@ std::string ComputeKey(OrthancPluginHttpMethod method, const AccessedResource& access, - const Token& token, + const Token* token, const std::string& tokenValue) const; std::string ComputeKey(const std::string& permission, - const Token& token, + const Token* token, const std::string& tokenValue) const; virtual bool IsGrantedInternal(unsigned int& validity, @@ -66,5 +66,33 @@ CachedAuthorizationService(BaseAuthorizationService* decorated /* takes ownership */, ICacheFactory& factory); + virtual bool HasUserProfile() const + { + return decorated_->HasUserProfile(); + } + + virtual bool HasCreateToken() const + { + return decorated_->HasCreateToken(); + } + + virtual bool HasTokenValidation() const + { + return decorated_->HasTokenValidation(); + } + + bool CreateToken(IAuthorizationService::CreatedToken& response, + const std::string& tokenType, + const std::string& id, + const std::vector& resources, + const std::string& expirationDateString) + { + return decorated_->CreateToken(response, + tokenType, + id, + resources, + expirationDateString); + } + }; } diff -r 30fb3ce960d9 -r e381ba725669 Plugin/DefaultConfiguration.json --- a/Plugin/DefaultConfiguration.json Wed Feb 22 13:13:38 2023 +0100 +++ b/Plugin/DefaultConfiguration.json Fri Feb 24 18:13:36 2023 +0100 @@ -1,10 +1,21 @@ { "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 Base URL of the auth webservice. This is an alias for all 3 next configurations: + // // "WebServiceUserProfileUrl" : " ROOT /user/get-profile", + // // "WebServiceTokenValidationUrl" : " ROOT /tokens/validate", + // // "WebServiceTokenCreationBaseUrl" : " ROOT /tokens/", + // "WebServiceRootUrl" : "http://change-me:8000/", + + // The URL of the auth webservice route implementing user profile (optional) + // (this configuration was previously named "WebService" and its old name is still accepted + // for backward compatibility) + // "WebServiceUserProfileUrl" : "http://change-me:8000/user/profile", + + // The URL of the auth webservice route implementing resource level authorization (optional) + // "WebServiceTokenValidationUrl" : "http://change-me:8000/tokens/validate", + + // The Base URL of the auth webservice route to create tokens (optional) + // "WebServiceTokenCreationBaseUrl" : "http://change-me:8000/tokens/", // The username and password to connect to the webservice (optional) //"WebServiceUsername": "change-me", @@ -32,7 +43,7 @@ //"UncheckedLevels" : [], // Definition of required "user-permissions". This can be fully customized. - // You may define other permissions yourself as long as they mathc the permissions + // You may define other permissions yourself as long as they match 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) diff -r 30fb3ce960d9 -r e381ba725669 Plugin/IAuthorizationService.h --- a/Plugin/IAuthorizationService.h Wed Feb 22 13:13:38 2023 +0100 +++ b/Plugin/IAuthorizationService.h Fri Feb 24 18:13:36 2023 +0100 @@ -31,6 +31,20 @@ class IAuthorizationService : public boost::noncopyable { public: + struct OrthancResource + { + std::string dicomUid; + std::string orthancId; + std::string url; + std::string level; + }; + + struct CreatedToken + { + std::string url; + std::string token; + }; + virtual ~IAuthorizationService() { } @@ -60,5 +74,15 @@ virtual bool HasAnonymousUserPermission(unsigned int& validity /* out */, const std::set& anyOfPermissions) = 0; + + virtual bool CreateToken(CreatedToken& response, + const std::string& tokenType, + const std::string& id, + const std::vector& resources, + const std::string& expirationDateString) = 0; + + virtual bool HasUserProfile() const = 0; + virtual bool HasCreateToken() const = 0; + virtual bool HasTokenValidation() const = 0; }; } diff -r 30fb3ce960d9 -r e381ba725669 Plugin/Plugin.cpp --- a/Plugin/Plugin.cpp Wed Feb 22 13:13:38 2023 +0100 +++ b/Plugin/Plugin.cpp Fri Feb 24 18:13:36 2023 +0100 @@ -49,6 +49,19 @@ return out; } +struct TokenAndValue +{ + const OrthancPlugins::Token& token; + std::string value; + + TokenAndValue(const OrthancPlugins::Token& token, const std::string& value) : + token(token), + value(value) + { + } +}; + + static int32_t FilterHttpRequests(OrthancPluginHttpMethod method, const char *uri, const char *ip, @@ -61,6 +74,8 @@ { try { + unsigned int validity; // ignored + if (method == OrthancPluginHttpMethod_Get) { // Allow GET accesses to static resources @@ -79,7 +94,35 @@ } } - unsigned int validity; // ignored + OrthancPlugins::AssociativeArray headers(headersCount, headersKeys, headersValues, false); + OrthancPlugins::AssociativeArray getArguments(getArgumentsCount, getArgumentsKeys, getArgumentsValues, true); + + std::vector authTokens; // the tokens that are set in this request + + for (std::set::const_iterator token = tokens_.begin(); token != tokens_.end(); ++token) + { + std::string value; + + bool hasValue = false; + switch (token->GetType()) + { + case OrthancPlugins::TokenType_HttpHeader: + hasValue = headers.GetValue(value, token->GetKey()); + break; + + case OrthancPlugins::TokenType_GetArgument: + hasValue = getArguments.GetValue(value, token->GetKey()); + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + if (hasValue) + { + authTokens.push_back(TokenAndValue(*token, value)); + } + } // check if the user permissions grants him access if (permissionParser_.get() != NULL && @@ -90,7 +133,7 @@ std::string matchedPattern; if (permissionParser_->Parse(requiredPermissions, matchedPattern, method, uri)) { - if (tokens_.empty()) + if (authTokens.empty()) { LOG(INFO) << "Testing whether anonymous user has any of the required permissions '" << JoinStrings(requiredPermissions) << "'"; if (authorizationService_->HasAnonymousUserPermission(validity, requiredPermissions)) @@ -100,25 +143,12 @@ } 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) + for (size_t i = 0; i < authTokens.size(); ++i) { - 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 '" << authTokens[i].token.GetKey() << "' HTTP header required to match '" << matchedPattern << "'"; + if (authorizationService_->HasUserPermission(validity, requiredPermissions, authTokens[i].token, authTokens[i].value)) { - 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; - } + return 1; } } } @@ -130,7 +160,6 @@ { // Parse the resources that are accessed through this URI OrthancPlugins::IAuthorizationParser::AccessedResources accesses; - OrthancPlugins::AssociativeArray getArguments(getArgumentsCount, getArgumentsKeys, getArgumentsValues, true); if (!authorizationParser_->Parse(accesses, uri, getArguments.GetMap())) { @@ -152,39 +181,16 @@ bool granted = false; - if (tokens_.empty()) + if (authTokens.empty()) { granted = authorizationService_->IsGrantedToAnonymousUser(validity, method, *access); } 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) + // Loop over all the authorization tokens in the request until finding one that is granted + for (size_t i = 0; i < authTokens.size(); ++i) { - std::string value; - - bool hasValue = false; - switch (token->GetType()) - { - case OrthancPlugins::TokenType_HttpHeader: - hasValue = headers.GetValue(value, token->GetKey()); - break; - - case OrthancPlugins::TokenType_GetArgument: - hasValue = getArguments.GetValue(value, token->GetKey()); - break; - - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); - } - - if (hasValue && - authorizationService_->IsGranted(validity, method, *access, *token, value)) + if (authorizationService_->IsGranted(validity, method, *access, authTokens[i].token, authTokens[i].value)) { granted = true; break; @@ -296,6 +302,103 @@ } } +void CreateToken(OrthancPluginRestOutput* output, + const char* /*url*/, + const OrthancPluginHttpRequest* request) +{ + OrthancPluginContext* context = OrthancPlugins::GetGlobalContext(); + + if (request->method != OrthancPluginHttpMethod_Put) + { + OrthancPluginSendMethodNotAllowed(context, output, "PUT"); + } + else + { + // The filtering to this route is performed by this plugin as it is done for any other route before we get here. + // Since the route contains the tokenType, we can allow/forbid creating them based on the url + + // simply forward the request to the auth-service + std::string tokenType; + if (request->groupsCount == 1) + { + tokenType = request->groups[0]; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + // convert from Orthanc flavored API to WebService API + Json::Value body; + if (!OrthancPlugins::ReadJson(body, request->body, request->bodySize)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "A JSON payload was expected"); + } + + std::string id; + std::vector resources; + std::string expirationDateString; + + if (body.isMember("ID")) + { + id = body["ID"].asString(); + } + + for (Json::ArrayIndex i = 0; i < body["Resources"].size(); ++i) + { + const Json::Value& jsonResource = body["Resources"][i]; + OrthancPlugins::IAuthorizationService::OrthancResource resource; + + if (jsonResource.isMember("DicomUid")) + { + resource.dicomUid = jsonResource["DicomUid"].asString(); + } + + if (jsonResource.isMember("OrthancId")) + { + resource.orthancId = jsonResource["OrthancId"].asString(); + } + + if (jsonResource.isMember("Url")) + { + resource.url = jsonResource["Url"].asString(); + } + + resource.level = jsonResource["Level"].asString(); + resources.push_back(resource); + } + + if (body.isMember("ExpirationDate")) + { + expirationDateString = body["ExpirationDate"].asString(); + } + + OrthancPlugins::IAuthorizationService::CreatedToken createdToken; + if (authorizationService_->CreateToken(createdToken, + tokenType, + id, + resources, + expirationDateString)) + { + Json::Value createdJsonToken; + createdJsonToken["Token"] = createdToken.token; + + if (!createdToken.url.empty()) + { + createdJsonToken["Url"] = createdToken.url; + } + else + { + createdJsonToken["Url"] = Json::nullValue; + } + + OrthancPlugins::AnswerJson(createdJsonToken, output); + } + + + } +} + void GetUserProfile(OrthancPluginRestOutput* output, const char* /*url*/, const OrthancPluginHttpRequest* request) @@ -314,7 +417,6 @@ OrthancPlugins::AssociativeArray getArguments (request->getCount, request->getKeys, request->getValues, true); - // Loop over all the authorization tokens stored in the HTTP // headers, until finding one that is granted for (std::set::const_iterator @@ -477,42 +579,78 @@ pluginConfiguration.LookupSetOfStrings(uncheckedResources_, "UncheckedResources", false); pluginConfiguration.LookupListOfStrings(uncheckedFolders_, "UncheckedFolders", false); - std::string url; + std::string urlTokenValidation; + std::string urlTokenCreationBase; + std::string urlUserProfile; + std::string urlRoot; - static const char* WEB_SERVICE = "WebService"; - if (!pluginConfiguration.LookupStringValue(url, WEB_SERVICE)) + static const char* WEB_SERVICE_ROOT = "WebServiceRootUrl"; + static const char* WEB_SERVICE_TOKEN_VALIDATION = "WebServiceTokenValidationUrl"; + static const char* WEB_SERVICE_TOKEN_CREATION_BASE = "WebServiceTokenCreationBaseUrl"; + static const char* WEB_SERVICE_USER_PROFILE = "WebServiceUserProfileUrl"; + static const char* WEB_SERVICE_TOKEN_VALIDATION_LEGACY = "WebService"; + if (pluginConfiguration.LookupStringValue(urlRoot, WEB_SERVICE_ROOT)) { - LOG(WARNING) << "Authorization plugin: no \"" << WEB_SERVICE << "\" configuration provided. Will not perform resource based authorization."; + urlTokenValidation = Orthanc::Toolbox::JoinUri(urlRoot, "/tokens/validate"); + urlTokenCreationBase = Orthanc::Toolbox::JoinUri(urlRoot, "/tokens/"); + urlUserProfile = Orthanc::Toolbox::JoinUri(urlRoot, "/user/get-profile"); } - else + else { + pluginConfiguration.LookupStringValue(urlTokenValidation, WEB_SERVICE_TOKEN_VALIDATION); + if (urlTokenValidation.empty()) + { + pluginConfiguration.LookupStringValue(urlTokenValidation, WEB_SERVICE_TOKEN_VALIDATION_LEGACY); + } + + pluginConfiguration.LookupStringValue(urlTokenCreationBase, WEB_SERVICE_TOKEN_CREATION_BASE); + pluginConfiguration.LookupStringValue(urlUserProfile, WEB_SERVICE_USER_PROFILE); + } + + if (!urlTokenValidation.empty()) + { + LOG(WARNING) << "Authorization plugin: url defined for Token Validation: " << urlTokenValidation; 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)) - { - LOG(WARNING) << "Authorization plugin: no \"" << WEB_SERVICE_USER_PROFILE << "\" configuration provided. Will not perform user-permissions based authorization."; - } else { + LOG(WARNING) << "Authorization plugin: no url defined for Token Validation"; + } + + if (!urlUserProfile.empty()) + { + LOG(WARNING) << "Authorization plugin: url defined for User Profile: " << urlUserProfile; + + static const char* PERMISSIONS = "Permissions"; 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"); + "\" option since you have defined the \"" + std::string(WEB_SERVICE_ROOT) + "\" option"); } permissionParser_.reset (new OrthancPlugins::PermissionParser(dicomWebRoot, oe2Root)); permissionParser_->Add(pluginConfiguration.GetJson()[PERMISSIONS]); } + else + { + LOG(WARNING) << "Authorization plugin: no url defined for User Profile"; + } + + if (!urlTokenCreationBase.empty()) + { + LOG(WARNING) << "Authorization plugin: base url defined for Token Creation : " << urlTokenCreationBase; + // TODO Token Creation + } + else + { + LOG(WARNING) << "Authorization plugin: no base url defined for Token Creation"; + } 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) + "\""); + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "Authorization plugin: No Token Validation or User Profile url defined"); } std::set standardConfigurations; @@ -597,7 +735,9 @@ } } - std::unique_ptr webService(new OrthancPlugins::AuthorizationWebService(url)); + std::unique_ptr webService(new OrthancPlugins::AuthorizationWebService(urlTokenValidation, + urlTokenCreationBase, + urlUserProfile)); std::string webServiceIdentifier; if (pluginConfiguration.LookupStringValue(webServiceIdentifier, "WebServiceIdentifier")) @@ -612,18 +752,24 @@ webService->SetCredentials(webServiceUsername, webServicePassword); } - std::string webServiceUserProfileUrl; - if (pluginConfiguration.LookupStringValue(webServiceUserProfileUrl, "WebServiceUserProfileUrl")) - { - webService->SetUserProfileUrl(webServiceUserProfileUrl); - } - authorizationService_.reset (new OrthancPlugins::CachedAuthorizationService (webService.release(), factory)); - OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback); - OrthancPlugins::RegisterRestCallback("/auth/user-profile", true); + if (!urlTokenValidation.empty()) + { + OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback); + } + + if (!urlUserProfile.empty()) + { + OrthancPlugins::RegisterRestCallback("/auth/user/profile", true); + } + + if (!urlTokenCreationBase.empty()) + { + OrthancPlugins::RegisterRestCallback("/auth/tokens/(.*)", true); + } #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 2, 1) OrthancPluginRegisterIncomingHttpRequestFilter2(context, FilterHttpRequests); diff -r 30fb3ce960d9 -r e381ba725669 README --- a/README Wed Feb 22 13:13:38 2023 +0100 +++ b/README Fri Feb 24 18:13:36 2023 +0100 @@ -24,6 +24,49 @@ http://book.orthanc-server.com/plugins/authorization.html +API +--- + +Since version 0.5.0, the plugin implements a RestA API to generate tokens +(provided that the Web service is able to do so). + +Sample Orthanc Flavored API: + +curl -X PUT http://localhost:8042/auth/tokens/resource-instant-link -H 'Content-Type: application/json' \ + -d '{"ID": "toto", + "Resources" : [{ + "DicomUid": "1.2", + "OrthancId": "", + "Level": "study" + }], + "Type": "resource-instant-link", + "ExpirationDate": "2026-12-31T11:00:00Z"}' + +Sample response: + { + "Token": "e148.....", + "Url": null + } + +The API that must be implemented by the webservice is slighlty different wrt naming conventions: + +curl -X PUT http://localhost:8000/tokens/resource-instant-link -H 'Content-Type: application/json' \ + -d '{"id": "toto", + "resources" : [{ + "dicom-uid": "1.2", + "level": "study" + }], + "type": "resource-instant-link", + "expiration-date": "2026-12-31T11:00:00Z"}' + +Sample response: + { + "token": "e148.....", + "url": null + } + + + Licensing ---------