Mercurial > hg > orthanc-authorization
changeset 252:f1c8f36e0b87 inbox
merge default -> inbox
author | Alain Mazy <am@orthanc.team> |
---|---|
date | Mon, 14 Jul 2025 11:40:39 +0200 |
parents | 2b09f8e98cfe (diff) e957ca443917 (current diff) |
children | b340ccd0b22a |
files | NEWS |
diffstat | 9 files changed, 1116 insertions(+), 152 deletions(-) [+] |
line wrap: on
line diff
--- a/NEWS Mon Jul 14 11:40:03 2025 +0200 +++ b/NEWS Mon Jul 14 11:40:39 2025 +0200 @@ -5,6 +5,17 @@ => Recommended SDK version: 1.12.4 <= => Minimum SDK version: 1.11.3 <= +* New configuration "ExtraPermissions" to ADD new permissions to + the default "Permissions" entries. +* Improved handling of "Anonymous" user profiles (when no auth-tokens + are provided): The plugin will now request the auth-service to + get an anonymous user profile even if there are no auth-tokens in the + HTTP request. +* The User profile can now contain a "groups" field if the auth-service + provides it. +* The User profile can now contain an "id" field if the auth-service + provides it. +* New experimental feature: audit-logs (TODO) * Fixed a security issue: the entries in the cache token->permissions were kept too long in the cache allowing users to have access to generic routes even with an expired token. These entries are now stored maximum for 10 seconds.
--- a/Plugin/AuthorizationWebService.cpp Mon Jul 14 11:40:03 2025 +0200 +++ b/Plugin/AuthorizationWebService.cpp Mon Jul 14 11:40:39 2025 +0200 @@ -34,6 +34,9 @@ static const char* PERMISSIONS = "permissions"; static const char* AUTHORIZED_LABELS = "authorized-labels"; static const char* USER_NAME = "name"; + static const char* GROUPS = "groups"; + static const char* USER_ID = "user-id"; + bool AuthorizationWebService::IsGrantedInternal(unsigned int& validity, @@ -339,8 +342,10 @@ { jsonProfile = Json::objectValue; jsonProfile[USER_NAME] = profile.name; + jsonProfile[USER_ID] = profile.userId; Orthanc::SerializationToolbox::WriteSetOfStrings(jsonProfile, profile.authorizedLabels, AUTHORIZED_LABELS); Orthanc::SerializationToolbox::WriteSetOfStrings(jsonProfile, profile.permissions, PERMISSIONS); + Orthanc::SerializationToolbox::WriteSetOfStrings(jsonProfile, profile.groups, GROUPS); } void AuthorizationWebService::FromJson(UserProfile& profile, const Json::Value& jsonProfile) @@ -368,6 +373,19 @@ { profile.authorizedLabels.insert(jsonProfile[AUTHORIZED_LABELS][i].asString()); } + + if (jsonProfile.isMember(GROUPS) && jsonProfile[GROUPS].isArray()) + { + for (Json::ArrayIndex i = 0; i < jsonProfile[GROUPS].size(); ++i) + { + profile.groups.insert(jsonProfile[GROUPS][i].asString()); + } + } + + if (jsonProfile.isMember(USER_ID) && jsonProfile[USER_ID].isString()) + { + profile.userId = jsonProfile[USER_ID].asString(); + } }
--- a/Plugin/BaseAuthorizationService.h Mon Jul 14 11:40:03 2025 +0200 +++ b/Plugin/BaseAuthorizationService.h Mon Jul 14 11:40:39 2025 +0200 @@ -100,24 +100,5 @@ return false; } - virtual bool HasAnonymousUserPermission(const std::set<std::string>& anyOfPermissions) ORTHANC_OVERRIDE - { - if (anyOfPermissions.size() == 0) - { - return true; - } - - UserProfile anonymousUserProfile; - anonymousUserProfile.tokenType = TokenType_None; - - for (std::set<std::string>::const_iterator it = anyOfPermissions.begin(); it != anyOfPermissions.end(); ++it) - { - if (HasUserPermissionInternal(*it, anonymousUserProfile)) - { - return true; - } - } - return false; - } }; }
--- a/Plugin/Enumerations.cpp Mon Jul 14 11:40:03 2025 +0200 +++ b/Plugin/Enumerations.cpp Mon Jul 14 11:40:39 2025 +0200 @@ -84,4 +84,31 @@ throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, std::string("Invalid access level: ") + tmp); } } + + OrthancPluginResourceType StringToResourceType(const char* type) + { + std::string s(type); + Orthanc::Toolbox::ToUpperCase(s); + + if (s == "PATIENT" || s == "PATIENTS") + { + return OrthancPluginResourceType_Patient; + } + else if (s == "STUDY" || s == "STUDIES") + { + return OrthancPluginResourceType_Study; + } + else if (s == "SERIES") + { + return OrthancPluginResourceType_Series; + } + else if (s == "INSTANCE" || s == "IMAGE" || + s == "INSTANCES" || s == "IMAGES") + { + return OrthancPluginResourceType_Instance; + } + + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, std::string("Invalid resource type '") + type + "'"); + } + }
--- a/Plugin/Enumerations.h Mon Jul 14 11:40:03 2025 +0200 +++ b/Plugin/Enumerations.h Mon Jul 14 11:40:39 2025 +0200 @@ -21,6 +21,7 @@ #pragma once #include <Enumerations.h> +#include <orthanc/OrthancCPlugin.h> namespace OrthancPlugins { @@ -46,4 +47,6 @@ std::string EnumerationToString(AccessLevel level); AccessLevel StringToAccessLevel(const std::string& level); + + OrthancPluginResourceType StringToResourceType(const char* type); }
--- a/Plugin/IAuthorizationService.h Mon Jul 14 11:40:03 2025 +0200 +++ b/Plugin/IAuthorizationService.h Mon Jul 14 11:40:39 2025 +0200 @@ -61,8 +61,10 @@ struct UserProfile { std::string name; + std::string userId; std::set<std::string> permissions; std::set<std::string> authorizedLabels; + std::set<std::string> groups; // the source token key/value that identified the user TokenType tokenType; @@ -95,8 +97,6 @@ virtual bool HasUserPermission(const std::set<std::string>& anyOfPermissions, const UserProfile& profile) = 0; - virtual bool HasAnonymousUserPermission(const std::set<std::string>& anyOfPermissions) = 0; - virtual bool CreateToken(CreatedToken& response, const std::string& tokenType, const std::string& id,
--- a/Plugin/Plugin.cpp Mon Jul 14 11:40:03 2025 +0200 +++ b/Plugin/Plugin.cpp Mon Jul 14 11:40:39 2025 +0200 @@ -38,6 +38,7 @@ // Configuration of the authorization plugin static bool resourceTokensEnabled_ = false; static bool userTokensEnabled_ = false; +static bool enableAuditLogs_ = true; static std::unique_ptr<OrthancPlugins::IAuthorizationParser> authorizationParser_; static std::unique_ptr<OrthancPlugins::IAuthorizationService> authorizationService_; static std::unique_ptr<OrthancPlugins::PermissionParser> permissionParser_; @@ -64,7 +65,77 @@ OrthancPluginSendHttpStatus(context, output, 403, message, strlen(message)); } +static const char* KEY_USER_DATA = "UserData"; +static const char* KEY_USER_ID = "AuditLogsUserId"; +static bool GetUserIdFromUserData(std::string& userId, const Json::Value& payload) +{ + if (payload.isMember(KEY_USER_DATA) && payload[KEY_USER_DATA].isObject() + && payload[KEY_USER_DATA].isMember(KEY_USER_ID) && payload[KEY_USER_DATA][KEY_USER_ID].isString()) + { + userId = payload[KEY_USER_DATA][KEY_USER_ID].asString(); + return true; + } + return false; +} + +static void SetUserIdInUserdata(Json::Value& payload, const std::string& userId) +{ + if (!payload.isMember(KEY_USER_DATA)) + { + payload[KEY_USER_DATA] = Json::objectValue; + } + payload[KEY_USER_DATA][KEY_USER_ID] = userId; +} + +struct AuditLog +{ + std::string userId; + OrthancPluginResourceType resourceType; + std::string resourceId; + std::string action; + Json::Value logData; + + AuditLog(const std::string& userId, + const OrthancPluginResourceType& resourceType, + const std::string& resourceId, + const std::string& action, + const Json::Value& logData) : + userId(userId), + resourceType(resourceType), + resourceId(resourceId), + action(action), + logData(logData) + { + } +}; + +static void RecordAuditLog(const std::string& userId, + const OrthancPluginResourceType& resourceType, + const std::string& resourceId, + const std::string& action, + const Json::Value& logData) +{ + LOG(WARNING) << "AUDIT-LOG: " << userId << " / " << action << " on " << resourceType << ":" << resourceId << ", " << logData.toStyledString(); +} + +static void RecordAuditLog(const AuditLog& auditLog) +{ + RecordAuditLog(auditLog.userId, + auditLog.resourceType, + auditLog.resourceId, + auditLog.action, + auditLog.logData); +} + + +static void RecordAuditLogs(const std::list<AuditLog>& auditLogs) +{ + for (std::list<AuditLog>::const_iterator it = auditLogs.begin(); it != auditLogs.end(); ++it) + { + RecordAuditLog(*it); + } +} class TokenAndValue { @@ -273,6 +344,46 @@ return false; } +static bool TestRequiredPermissions(bool& hasUserRequiredPermissions, + const std::set<std::string>& requiredPermissions, + const OrthancPlugins::IAuthorizationService::UserProfile& profile, + const std::string& msg, + const char* uri, + OrthancPluginHttpMethod method, + const OrthancPlugins::AssociativeArray& getArguments + ) +{ + if (authorizationService_->HasUserPermission(requiredPermissions, profile)) + { + LOG(INFO) << msg << " -> granted to user '" << profile.name << "'"; + hasUserRequiredPermissions = true; + + // check labels permissions + std::string msg2 = std::string("Testing whether user has the authorized_labels to access '") + uri + "'"; + + bool hasAuthorizedLabelsForResource = false; + if (CheckAuthorizedLabelsForResource(hasAuthorizedLabelsForResource, uri, method, getArguments, profile)) + { + if (hasAuthorizedLabelsForResource) + { + LOG(INFO) << msg2 << " -> granted"; + } + else + { + LOG(INFO) << msg2 << " -> not granted"; + return false; // the labels for this resource prevents access -> stop checking now ! + } + } + } + else + { + LOG(INFO) << msg << " -> not granted"; + hasUserRequiredPermissions = false; + } + + return true; +} + static int32_t FilterHttpRequests(OrthancPluginHttpMethod method, const char *uri, const char *ip, @@ -313,6 +424,7 @@ // Based on the tokens, check if the user has access based on its permissions and the mapping between urls and permissions //////////////////////////////////////////////////////////////// bool hasUserRequiredPermissions = false; + std::string userId; if (permissionParser_.get() != NULL && authorizationService_.get() != NULL) @@ -323,21 +435,20 @@ { if (authTokens.empty()) { - std::string msg = std::string("Testing whether anonymous user has any of the required permissions '") + JoinStrings(requiredPermissions) + "'"; - + std::string msg = std::string("Testing whether anonymous user has any of the required permissions '") + JoinStrings(requiredPermissions) + "' required to match '" + matchedPattern + "'"; + + OrthancPlugins::IAuthorizationService::UserProfile anonymousProfile; + unsigned int validityNotUsed; + authorizationService_->GetUserProfile(validityNotUsed, anonymousProfile, OrthancPlugins::Token(OrthancPlugins::TokenType_None, ""), ""); + userId = "anonymous"; + LOG(INFO) << msg; - - if (authorizationService_->HasAnonymousUserPermission(requiredPermissions)) + if (!TestRequiredPermissions(hasUserRequiredPermissions, requiredPermissions, anonymousProfile, msg, uri, method, getArguments)) { - LOG(INFO) << msg << " -> granted"; - hasUserRequiredPermissions = true; + return 0; // the labels for this resource prevents access -> stop checking now ! } - else - { - LOG(INFO) << msg << " -> not granted"; - hasUserRequiredPermissions = false; - // continue in order to check if there is a resource token that could grant access to the resource - } + + // continue in order to check if there is a resource token that could grant access to the resource } else { @@ -345,38 +456,21 @@ { std::string msg = std::string("Testing whether user has the required permissions '") + JoinStrings(requiredPermissions) + "' based on the HTTP header '" + authTokens[i].GetToken().GetKey() + "' required to match '" + matchedPattern + "'"; - // LOG(INFO) << msg; + LOG(INFO) << msg; OrthancPlugins::IAuthorizationService::UserProfile profile; unsigned int validityNotUsed; authorizationService_->GetUserProfile(validityNotUsed, profile, authTokens[i].GetToken(), authTokens[i].GetValue()); - if (authorizationService_->HasUserPermission(requiredPermissions, profile)) + if (!profile.userId.empty()) { - LOG(INFO) << msg << " -> granted"; - hasUserRequiredPermissions = true; - - // check labels permissions - msg = std::string("Testing whether user has the authorized_labels to access '") + uri + "' based on the HTTP header '" + authTokens[i].GetToken().GetKey() + "'"; + userId = profile.userId; + } - bool hasAuthorizedLabelsForResource = false; - if (CheckAuthorizedLabelsForResource(hasAuthorizedLabelsForResource, uri, method, getArguments, profile)) - { - if (hasAuthorizedLabelsForResource) - { - LOG(INFO) << msg << " -> granted"; - } - else - { - LOG(INFO) << msg << " -> not granted"; - return 0; // the labels for this resource prevents access -> stop checking now ! - } - } + if (!TestRequiredPermissions(hasUserRequiredPermissions, requiredPermissions, profile, msg, uri, method, getArguments)) + { + return 0; // the labels for this resource prevents access -> stop checking now ! } - else - { - LOG(INFO) << msg << " -> not granted"; - hasUserRequiredPermissions = false; - } + } } } @@ -478,34 +572,96 @@ { try { - if (authorizationParser_.get() == NULL) + if (authorizationParser_.get() == NULL || !enableAuditLogs_) { return OrthancPluginErrorCode_Success; } - - if (changeType == OrthancPluginChangeType_Deleted) + + switch(changeType) { - switch (resourceType) + case OrthancPluginChangeType_JobSuccess: { - case OrthancPluginResourceType_Patient: - authorizationParser_->Invalidate(Orthanc::ResourceType_Patient, resourceId); - break; + Json::Value job; + if (OrthancPlugins::RestApiGet(job, std::string("/jobs/") + resourceId, false)) + { + if (job["Type"].asString() == "ResourceModification") + { + Json::Value jobContent = job["Content"]; + std::string sourceResourceId = jobContent["ParentResources"][0].asString(); + std::string modifiedResourceId = jobContent["ID"].asString(); + OrthancPluginResourceType resourceType = OrthancPlugins::StringToResourceType(jobContent["Type"].asString().c_str()); + + bool isAnonymization = jobContent.isMember("IsAnonymization") && jobContent["IsAnonymization"].asBool(); + LOG(WARNING) << jobContent.toStyledString(); + + if (isAnonymization) + { + std::string userId; + if (GetUserIdFromUserData(userId, job)) + { + // attach a log to the source study + Json::Value logData; + logData["ModifiedResourceId"] = modifiedResourceId; + logData["ModifiedResourceType"] = resourceType; + + RecordAuditLog(userId, + resourceType, + sourceResourceId, + (isAnonymization ? "success-anonymization" : "success-modification-job"), + logData); + + // attach a log to the modified study + if (sourceResourceId != modifiedResourceId) + { + Json::Value logData; + logData["SourceResourceId"] = sourceResourceId; + logData["SourceResourceType"] = resourceType; - case OrthancPluginResourceType_Study: - authorizationParser_->Invalidate(Orthanc::ResourceType_Study, resourceId); - break; + RecordAuditLog(userId, + resourceType, + modifiedResourceId, + (isAnonymization ? "new-study-from-anonymization-job" : "new-study-from-modification-job"), + logData); + } + } + } + } - case OrthancPluginResourceType_Series: - authorizationParser_->Invalidate(Orthanc::ResourceType_Series, resourceId); - break; + } + + return OrthancPluginErrorCode_Success; + } + case OrthancPluginChangeType_JobFailure: + { + return OrthancPluginErrorCode_Success; + } - case OrthancPluginResourceType_Instance: - authorizationParser_->Invalidate(Orthanc::ResourceType_Instance, resourceId); - break; + case OrthancPluginChangeType_Deleted: + { + switch (resourceType) + { + case OrthancPluginResourceType_Patient: + authorizationParser_->Invalidate(Orthanc::ResourceType_Patient, resourceId); + break; + + case OrthancPluginResourceType_Study: + authorizationParser_->Invalidate(Orthanc::ResourceType_Study, resourceId); + break; - default: - break; + case OrthancPluginResourceType_Series: + authorizationParser_->Invalidate(Orthanc::ResourceType_Series, resourceId); + break; + + case OrthancPluginResourceType_Instance: + authorizationParser_->Invalidate(Orthanc::ResourceType_Instance, resourceId); + break; + + default: + break; + } } + default: + return OrthancPluginErrorCode_Success; } return OrthancPluginErrorCode_Success; @@ -527,6 +683,47 @@ } } +bool GetUserProfileInternal_(OrthancPlugins::IAuthorizationService::UserProfile& profile, + const OrthancPlugins::AssociativeArray& headers, + const OrthancPlugins::AssociativeArray& getArguments, + bool ignoreEmptyValues) +{ + for (std::set<OrthancPlugins::Token>::const_iterator + token = tokens_.begin(); token != tokens_.end(); ++token) + { + OrthancPlugins::IAuthorizationService::UserProfile tryProfile; + + std::string value; + switch (token->GetType()) + { + case OrthancPlugins::TokenType_HttpHeader: + headers.GetValue(value, token->GetKey()); + break; + + case OrthancPlugins::TokenType_GetArgument: + getArguments.GetValue(value, token->GetKey()); + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + if (ignoreEmptyValues && value.empty()) + { + continue; + } + + unsigned int validity; // not used + if (authorizationService_->GetUserProfile(validity, tryProfile, *token, value)) + { + profile = tryProfile; + return true; + } + } + + return false; +} + bool GetUserProfileInternal(OrthancPlugins::IAuthorizationService::UserProfile& profile, const OrthancPluginHttpRequest* request) { @@ -537,41 +734,14 @@ (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<OrthancPlugins::Token>::const_iterator - token = tokens_.begin(); token != tokens_.end(); ++token) + // headers, until finding one that is granted. + // But, first process only the tokens with a value to avoid getting identified as anonymous too fast ! + if (GetUserProfileInternal_(profile, headers, getArguments, true)) { - OrthancPlugins::IAuthorizationService::UserProfile tryProfile; - - 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) - { - unsigned int validity; // not used - if (authorizationService_->GetUserProfile(validity, tryProfile, *token, value)) - { - profile = tryProfile; - return true; - } - } + return true; } - return false; + return GetUserProfileInternal_(profile, headers, getArguments, false); } void AdjustToolsFindQueryLabels(Json::Value& query, const OrthancPlugins::IAuthorizationService::UserProfile& profile) @@ -921,6 +1091,325 @@ ToolsFindOrCountResources(output, url, request, "/tools/count-resources", false); } +void UploadInstancesWithAuditLogs(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + OrthancPluginContext* context = OrthancPlugins::GetGlobalContext(); + + // always forward to core + OrthancPlugins::RestApiClient coreApi(url, request); + coreApi.ExecuteAndForwardAnswer(context, output); + + if (request->method == OrthancPluginHttpMethod_Post) + { + OrthancPlugins::IAuthorizationService::UserProfile profile; + Json::Value coreResponse; + + if (GetUserProfileInternal(profile, request) && coreApi.GetAnswerJson(coreResponse)) + { + RecordAuditLog(profile.userId, OrthancPluginResourceType_Study, coreResponse["ParentStudy"].asString(), "uploaded-instance", coreResponse["ID"].asString()); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ForbiddenAccess, "Auth plugin: no user profile found, unable to handle POST to /instances with audit logs enabled."); + } + } +} + + +void ModifyAnonymizeWithAuditLogs(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request, + bool isModification) +{ + OrthancPluginContext* context = OrthancPlugins::GetGlobalContext(); + OrthancPluginResourceType resourceType = OrthancPlugins::StringToResourceType(request->groups[0]); + std::string resourceId = request->groups[1]; + + OrthancPlugins::RestApiClient coreApi(url, request); + + Json::Value payload; + if (!OrthancPlugins::ReadJson(payload, request->body, request->bodySize)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "A JSON payload was expected"); + } + + // Either there is a userId in UserData or the request comes from a user with profile + OrthancPlugins::IAuthorizationService::UserProfile profile; + std::string userId; + + // LOG(WARNING) << payload.toStyledString(); + + if (GetUserProfileInternal(profile, request) && !profile.userId.empty()) + { + userId = profile.userId; + } + else if (!GetUserIdFromUserData(userId, payload)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ForbiddenAccess, "Auth plugin: no user profile or UserData found, unable to handle anonymize/modify with audit logs enabled."); + } + + if ((payload.isMember("Synchronous") && !payload["Synchronous"].asBool()) + || (payload.isMember("Asynchronous") && !payload["Asynchronous"].asBool())) + { + // add UserData to the job payload to know who has modified the data. The handling of the log will then happen in the OnChange handler + SetUserIdInUserdata(payload, userId); + + // in any case, record that this resource is being modified/anonymized and record the payload + RecordAuditLog(userId, + resourceType, + resourceId, + (isModification ? "start-modification-job" : "start-anonymization-job"), + payload); + + if (coreApi.Execute()) + { + coreApi.ForwardAnswer(context, output); + } + } + else + { + Json::Value coreResponse; + + // if it is synchronous, perform the modification and record the log directly + if (coreApi.Execute()) + { + coreApi.ForwardAnswer(context, output); + } + + if (coreApi.GetAnswerJson(coreResponse)) + { + LOG(WARNING) << "TODO AUDIT-LOG " << coreResponse.toStyledString(); // TODO + } + } +} + +void ModifyWithAuditLogs(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + ModifyAnonymizeWithAuditLogs(output, url, request, true); +} + +void AnonymizeWithAuditLogs(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + ModifyAnonymizeWithAuditLogs(output, url, request, false); +} + +void LabelWithAuditLogs(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + OrthancPluginContext* context = OrthancPlugins::GetGlobalContext(); + OrthancPluginResourceType resourceType = OrthancPlugins::StringToResourceType(request->groups[0]); + std::string resourceId = request->groups[1]; + std::string label = request->groups[2]; + + OrthancPlugins::RestApiClient coreApi(url, request); + + if (request->method == OrthancPluginHttpMethod_Get && coreApi.Execute()) + { + coreApi.ForwardAnswer(context, output); + return; + } + else + { + OrthancPlugins::IAuthorizationService::UserProfile profile; + std::string userId; + + if (GetUserProfileInternal(profile, request) && !profile.userId.empty()) + { + userId = profile.userId; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ForbiddenAccess, "Auth plugin: no user profile or UserData found, unable to delete/put label with audit logs enabled."); + } + + std::string action; + if (request->method == OrthancPluginHttpMethod_Delete) + { + action = "deleted-label"; + } + else if (request->method == OrthancPluginHttpMethod_Put) + { + action = "added-label"; + } + + if (coreApi.Execute()) + { + RecordAuditLog(userId, + resourceType, + resourceId, + action, + label); + + coreApi.ForwardAnswer(context, output); + return; + } + } +} + + +OrthancPluginResourceType IdentifyResourceType(const std::string& resourceId) +{ + Json::Value v; + + if (OrthancPlugins::RestApiGet(v, "/studies/" + resourceId, false)) + { + return OrthancPluginResourceType_Study; + } + if (OrthancPlugins::RestApiGet(v, "/patients/" + resourceId, false)) + { + return OrthancPluginResourceType_Patient; + } + if (OrthancPlugins::RestApiGet(v, "/series/" + resourceId, false)) + { + return OrthancPluginResourceType_Series; + } + if (OrthancPlugins::RestApiGet(v, "/instances/" + resourceId, false)) + { + return OrthancPluginResourceType_Instance; + } + + return OrthancPluginResourceType_None; +} + + +void GetResourceDeletionAuditLogs(std::list<AuditLog>& auditLogs, + OrthancPluginResourceType resourceType, + const std::string& resourceId, + const OrthancPluginHttpRequest* request) +{ + OrthancPlugins::IAuthorizationService::UserProfile profile; + std::string userId; + + if (GetUserProfileInternal(profile, request) && !profile.userId.empty()) + { + userId = profile.userId; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ForbiddenAccess, "Auth plugin: no user profile or UserData found, unable to delete a resource with audit logs enabled."); + } + + Json::Value logData; + + switch (resourceType) + { + case OrthancPluginResourceType_Patient: + { + auditLogs.push_back(AuditLog(userId, resourceType, resourceId, "deleted-patient", logData)); + + // find all child studies and add the log to each of them + Json::Value patient; + if (OrthancPlugins::RestApiGet(patient, "/patients/" + resourceId, false)) + { + for (Json::ArrayIndex i = 0; i < patient["Studies"].size(); ++i) + { + auditLogs.push_back(AuditLog(userId, OrthancPluginResourceType_Study, patient["Studies"][i].asString(), "deleted-parent-patient", Json::nullValue)); + } + } + + }; break; + case OrthancPluginResourceType_Study: + { + auditLogs.push_back(AuditLog(userId, resourceType, resourceId, "deleted-study", logData)); + }; break; + case OrthancPluginResourceType_Series: + { + auditLogs.push_back(AuditLog(userId, resourceType, resourceId, "deleted-series", logData)); + + // add a log in the parent study + Json::Value parentStudy; + if (OrthancPlugins::RestApiGet(parentStudy, "/series/" + resourceId + "/study", false)) + { + auditLogs.push_back(AuditLog(userId, OrthancPluginResourceType_Study, parentStudy["ID"].asString(), "deleted-child-series", Json::nullValue)); + } + }; break; + case OrthancPluginResourceType_Instance: + { + auditLogs.push_back(AuditLog(userId, resourceType, resourceId, "deleted-instance", logData)); + + // add a log in the parent study + Json::Value parentStudy; + if (OrthancPlugins::RestApiGet(parentStudy, "/instances/" + resourceId + "/study", false)) + { + auditLogs.push_back(AuditLog(userId, OrthancPluginResourceType_Study, parentStudy["ID"].asString(), "deleted-child-instance", Json::nullValue)); + } + }; break; + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } +} + +void DeleteResourceWithAuditLogs(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + assert(request->method == OrthancPluginHttpMethod_Delete); + + OrthancPluginContext* context = OrthancPlugins::GetGlobalContext(); + OrthancPluginResourceType resourceType = OrthancPlugins::StringToResourceType(request->groups[0]); + std::string resourceId = request->groups[1]; + + std::list<AuditLog> auditLogs; + GetResourceDeletionAuditLogs(auditLogs, resourceType, resourceId, request); + + OrthancPlugins::RestApiClient coreApi(url, request); + + if (coreApi.Execute()) + { + RecordAuditLogs(auditLogs); + + coreApi.ForwardAnswer(context, output); + return; + } +} + + +void BulkDeleteWithAuditLogs(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + OrthancPluginContext* context = OrthancPlugins::GetGlobalContext(); + + if (request->method != OrthancPluginHttpMethod_Post) + { + OrthancPluginSendMethodNotAllowed(context, output, "POST"); + } + + Json::Value payload; + if (!OrthancPlugins::ReadJson(payload, request->body, request->bodySize) || !payload.isMember("Resources")) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "A JSON payload was expected"); + } + + std::list<AuditLog> auditLogs; + + for (Json::ArrayIndex i = 0; i < payload["Resources"].size(); ++i) + { + std::string resourceId = payload["Resources"][i].asString(); + OrthancPluginResourceType resourceType = IdentifyResourceType(resourceId); + GetResourceDeletionAuditLogs(auditLogs, resourceType, resourceId, request); + } + + OrthancPlugins::RestApiClient coreApi(url, request); + + if (coreApi.Execute()) + { + RecordAuditLogs(auditLogs); + + coreApi.ForwardAnswer(context, output); + return; + } + +} + + void ToolsLabels(OrthancPluginRestOutput* output, const char* /*url*/, const OrthancPluginHttpRequest* request) @@ -990,7 +1479,7 @@ if (request->method != OrthancPluginHttpMethod_Get) { OrthancPlugins::RestApiClient coreApi(url, request); - coreApi.Forward(context, output); + coreApi.ExecuteAndForwardAnswer(context, output); } else { @@ -1020,6 +1509,20 @@ FilterLabelsFromGetCoreUrl(output, url, request, FilterLabelsInResourceObject); } +void GetOrDeleteMainResource(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + if (enableAuditLogs_ && request->method == OrthancPluginHttpMethod_Delete) + { + DeleteResourceWithAuditLogs(output, url, request); + } + else + { + FilterLabelsFromSingleResource(output, url, request); + } +} + void FilterLabelsFromResourceList(OrthancPluginRestOutput* output, const char* url, const OrthancPluginHttpRequest* request) @@ -1216,6 +1719,8 @@ Json::Value jsonProfile; jsonProfile["name"] = profile.name; jsonProfile["permissions"] = Json::arrayValue; + jsonProfile["groups"] = Json::arrayValue; + for (std::set<std::string>::const_iterator it = profile.permissions.begin(); it != profile.permissions.end(); ++it) { jsonProfile["permissions"].append(*it); @@ -1224,6 +1729,15 @@ { jsonProfile["authorized-labels"].append(*it); } + for (std::set<std::string>::const_iterator it = profile.groups.begin(); it != profile.groups.end(); ++it) + { + jsonProfile["groups"].append(*it); + } + + if (!profile.userId.empty()) + { + jsonProfile["user-id"] = profile.userId; + } OrthancPlugins::AnswerJson(jsonProfile, output); } @@ -1494,6 +2008,12 @@ (new OrthancPlugins::PermissionParser(dicomWebRoot, oe2Root)); permissionParser_->Add(pluginConfiguration.GetJson()[PERMISSIONS], authorizationParser_.get()); + + static const char* const EXTRA_PERMISSIONS = "ExtraPermissions"; + if (pluginConfiguration.GetJson().isMember(EXTRA_PERMISSIONS)) + { + permissionParser_->Add(pluginConfiguration.GetJson()[EXTRA_PERMISSIONS], authorizationParser_.get()); + } } else { @@ -1663,7 +2183,7 @@ (new OrthancPlugins::CachedAuthorizationService (webService.release(), factory)); - if (!urlTokenValidation.empty()) + if (!urlTokenValidation.empty() || enableAuditLogs_) { OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback); } @@ -1682,10 +2202,8 @@ OrthancPlugins::RegisterRestCallback<AuthSettingsRoles>("/auth/settings/roles", true); OrthancPlugins::RegisterRestCallback<GetPermissionList>("/auth/settings/permissions", true); - OrthancPlugins::RegisterRestCallback<FilterLabelsFromSingleResource>("/instances/([^/]*)", true); - OrthancPlugins::RegisterRestCallback<FilterLabelsFromSingleResource>("/series/([^/]*)", true); - OrthancPlugins::RegisterRestCallback<FilterLabelsFromSingleResource>("/studies/([^/]*)", true); - OrthancPlugins::RegisterRestCallback<FilterLabelsFromSingleResource>("/patients/([^/]*)", true); + OrthancPlugins::RegisterRestCallback<GetOrDeleteMainResource>("/(patients|studies|series)/([^/]*)", true); // this includes auditLogs + OrthancPlugins::RegisterRestCallback<FilterLabelsFromSingleResource>("/instances/([^/]*)/patient", true); OrthancPlugins::RegisterRestCallback<FilterLabelsFromSingleResource>("/instances/([^/]*)/study", true); OrthancPlugins::RegisterRestCallback<FilterLabelsFromSingleResource>("/instances/([^/]*)/series", true); @@ -1704,6 +2222,24 @@ OrthancPlugins::RegisterRestCallback<FilterLabelsFromResourceList>("/patients/([^/]*)/instances", true); OrthancPlugins::RegisterRestCallback<FilterLabelsFromResourceList>("/patients/([^/]*)/series", true); OrthancPlugins::RegisterRestCallback<FilterLabelsFromResourceList>("/patients/([^/]*)/studies", true); + + if (enableAuditLogs_) + { + OrthancPlugins::RegisterRestCallback<UploadInstancesWithAuditLogs>("/instances", true); + OrthancPlugins::RegisterRestCallback<AnonymizeWithAuditLogs>("/(patients|studies|series)/([^/]*)/anonymize", true); + OrthancPlugins::RegisterRestCallback<ModifyWithAuditLogs>("/(patients|studies|series)/([^/]*)/modify", true); + OrthancPlugins::RegisterRestCallback<LabelWithAuditLogs>("/(patients|studies|series)/([^/]*)/labels/([^/]*)", true); + OrthancPlugins::RegisterRestCallback<BulkDeleteWithAuditLogs>("/tools/bulk-delete", true); + + // TODO + // OrthancPlugins::RegisterRestCallback<BulkModifyWithAuditLogs>("/tools/bulk-modify", true); + // /modalities/move + // /modalities/store + // /archive + create-archive + // /media + create-media + create-media-extended + // /bulk-anonymize + bulkd-modify + + } } if (!urlTokenCreationBase.empty())
--- a/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp Mon Jul 14 11:40:03 2025 +0200 +++ b/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp Mon Jul 14 11:40:39 2025 +0200 @@ -220,28 +220,6 @@ } -#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) - MemoryBuffer::MemoryBuffer(const void* buffer, - size_t size) - { - uint32_t s = static_cast<uint32_t>(size); - if (static_cast<size_t>(s) != size) - { - ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory); - } - else if (OrthancPluginCreateMemoryBuffer(GetGlobalContext(), &buffer_, s) != - OrthancPluginErrorCode_Success) - { - ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory); - } - else - { - memcpy(buffer_.data, buffer, size); - } - } -#endif - - void MemoryBuffer::Clear() { if (buffer_.data != NULL) @@ -253,6 +231,41 @@ } +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + void MemoryBuffer::Assign(const void* buffer, + size_t size) + { + uint32_t s = static_cast<uint32_t>(size); + if (static_cast<size_t>(s) != size) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory); + } + + Clear(); + + if (OrthancPluginCreateMemoryBuffer(GetGlobalContext(), &buffer_, s) != + OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory); + } + else + { + if (size > 0) + { + memcpy(buffer_.data, buffer, size); + } + } + } +#endif + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + void MemoryBuffer::Assign(const std::string& s) + { + Assign(s.empty() ? NULL : s.c_str(), s.size()); + } +#endif + + void MemoryBuffer::Assign(OrthancPluginMemoryBuffer& other) { Clear(); @@ -673,7 +686,7 @@ { OrthancString str; str.Assign(OrthancPluginDicomBufferToJson - (GetGlobalContext(), GetData(), GetSize(), format, flags, maxStringLength)); + (GetGlobalContext(), reinterpret_cast<const char*>(GetData()), GetSize(), format, flags, maxStringLength)); str.ToJson(target); } @@ -1566,7 +1579,7 @@ { if (!answer.IsEmpty()) { - result.assign(answer.GetData(), answer.GetSize()); + result.assign(reinterpret_cast<const char*>(answer.GetData()), answer.GetSize()); } return true; } @@ -2052,6 +2065,26 @@ DoPost(target, index, uri, body, headers)); } + bool OrthancPeers::DoPost(Json::Value& target, + size_t index, + const std::string& uri, + const std::string& body, + const HttpHeaders& headers, + unsigned int timeout) const + { + MemoryBuffer buffer; + + if (DoPost(buffer, index, uri, body, headers, timeout)) + { + buffer.ToJson(target); + return true; + } + else + { + return false; + } + } + bool OrthancPeers::DoPost(Json::Value& target, size_t index, @@ -2099,6 +2132,17 @@ const std::string& body, const HttpHeaders& headers) const { + return DoPost(target, index, uri, body, headers, timeout_); + } + + + bool OrthancPeers::DoPost(MemoryBuffer& target, + size_t index, + const std::string& uri, + const std::string& body, + const HttpHeaders& headers, + unsigned int timeout) const + { if (index >= index_.size()) { ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange); @@ -2117,7 +2161,7 @@ OrthancPluginErrorCode code = OrthancPluginCallPeerApi (GetGlobalContext(), *answer, NULL, &status, peers_, static_cast<uint32_t>(index), OrthancPluginHttpMethod_Post, uri.c_str(), - pluginHeaders.GetSize(), pluginHeaders.GetKeys(), pluginHeaders.GetValues(), body.empty() ? NULL : body.c_str(), body.size(), timeout_); + pluginHeaders.GetSize(), pluginHeaders.GetKeys(), pluginHeaders.GetValues(), body.empty() ? NULL : body.c_str(), body.size(), timeout); if (code == OrthancPluginErrorCode_Success) { @@ -4168,6 +4212,11 @@ { path_ += "?" + getArguments; } + + if (request->bodySize > 0 && request->body != NULL) + { + requestBody_.assign(reinterpret_cast<const char*>(request->body), request->bodySize); + } } #endif @@ -4235,9 +4284,17 @@ } } - void RestApiClient::Forward(OrthancPluginContext* context, OrthancPluginRestOutput* output) - { - if (Execute() && httpStatus_ == 200) + void RestApiClient::ExecuteAndForwardAnswer(OrthancPluginContext* context, OrthancPluginRestOutput* output) + { + if (Execute()) + { + ForwardAnswer(context, output); + } + } + + void RestApiClient::ForwardAnswer(OrthancPluginContext* context, OrthancPluginRestOutput* output) + { + if (httpStatus_ == 200) { const char* mimeType = NULL; for (HttpHeaders::const_iterator h = answerHeaders_.begin(); h != answerHeaders_.end(); ++h) @@ -4316,4 +4373,209 @@ } } #endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + KeyValueStore::Iterator::Iterator(OrthancPluginKeysValuesIterator *iterator) : + iterator_(iterator) + { + if (iterator_ == NULL) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + KeyValueStore::Iterator::~Iterator() + { + OrthancPluginFreeKeysValuesIterator(OrthancPlugins::GetGlobalContext(), iterator_); + } +#endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + bool KeyValueStore::Iterator::Next() + { + uint8_t done; + OrthancPluginErrorCode code = OrthancPluginKeysValuesIteratorNext(OrthancPlugins::GetGlobalContext(), &done, iterator_); + + if (code != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + else + { + return (done != 0); + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + std::string KeyValueStore::Iterator::GetKey() const + { + const char* s = OrthancPluginKeysValuesIteratorGetKey(OrthancPlugins::GetGlobalContext(), iterator_); + if (s == NULL) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + else + { + return s; + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + void KeyValueStore::Iterator::GetValue(std::string& value) const + { + OrthancPlugins::MemoryBuffer valueBuffer; + OrthancPluginErrorCode code = OrthancPluginKeysValuesIteratorGetValue(OrthancPlugins::GetGlobalContext(), *valueBuffer, iterator_); + + if (code != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + else + { + valueBuffer.ToString(value); + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + void KeyValueStore::Store(const std::string& key, + const void* value, + size_t valueSize) + { + if (static_cast<size_t>(static_cast<uint32_t>(valueSize)) != valueSize) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory); + } + + OrthancPluginErrorCode code = OrthancPluginStoreKeyValue(OrthancPlugins::GetGlobalContext(), storeId_.c_str(), + key.c_str(), value, static_cast<uint32_t>(valueSize)); + if (code != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + bool KeyValueStore::GetValue(std::string& value, + const std::string& key) + { + uint8_t found = false; + OrthancPlugins::MemoryBuffer valueBuffer; + OrthancPluginErrorCode code = OrthancPluginGetKeyValue(OrthancPlugins::GetGlobalContext(), &found, + *valueBuffer, storeId_.c_str(), key.c_str()); + + if (code != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + else if (found) + { + valueBuffer.ToString(value); + return true; + } + else + { + return false; + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + void KeyValueStore::DeleteKey(const std::string& key) + { + OrthancPluginErrorCode code = OrthancPluginDeleteKeyValue(OrthancPlugins::GetGlobalContext(), + storeId_.c_str(), key.c_str()); + + if (code != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + KeyValueStore::Iterator* KeyValueStore::CreateIterator() + { + return new Iterator(OrthancPluginCreateKeysValuesIterator(OrthancPlugins::GetGlobalContext(), storeId_.c_str())); + } +#endif + + +#if HAS_ORTHANC_PLUGIN_QUEUES == 1 + void Queue::Enqueue(const void* value, + size_t valueSize) + { + if (static_cast<size_t>(static_cast<uint32_t>(valueSize)) != valueSize) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory); + } + + OrthancPluginErrorCode code = OrthancPluginEnqueueValue(OrthancPlugins::GetGlobalContext(), + queueId_.c_str(), value, static_cast<uint32_t>(valueSize)); + + if (code != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_QUEUES == 1 + bool Queue::DequeueInternal(std::string& value, + OrthancPluginQueueOrigin origin) + { + uint8_t found = false; + OrthancPlugins::MemoryBuffer valueBuffer; + + OrthancPluginErrorCode code = OrthancPluginDequeueValue(OrthancPlugins::GetGlobalContext(), &found, + *valueBuffer, queueId_.c_str(), origin); + + if (code != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + else if (found) + { + valueBuffer.ToString(value); + return true; + } + else + { + return false; + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_QUEUES == 1 + uint64_t Queue::GetSize() + { + uint64_t size = 0; + OrthancPluginErrorCode code = OrthancPluginGetQueueSize(OrthancPlugins::GetGlobalContext(), queueId_.c_str(), &size); + + if (code != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + else + { + return size; + } + } +#endif }
--- a/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h Mon Jul 14 11:40:03 2025 +0200 +++ b/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h Mon Jul 14 11:40:39 2025 +0200 @@ -134,6 +134,14 @@ # define HAS_ORTHANC_PLUGIN_LOG_MESSAGE 0 #endif +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 8) +# define HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES 1 +# define HAS_ORTHANC_PLUGIN_QUEUES 1 +#else +# define HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES 0 +# define HAS_ORTHANC_PLUGIN_QUEUES 0 +#endif + // Macro to tag a function as having been deprecated #if (__cplusplus >= 201402L) // C++14 @@ -203,13 +211,6 @@ public: MemoryBuffer(); -#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) - // This constructor makes a copy of the given buffer in the memory - // handled by the Orthanc core - MemoryBuffer(const void* buffer, - size_t size); -#endif - ~MemoryBuffer() { Clear(); @@ -220,6 +221,16 @@ return &buffer_; } +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + // Copy of the given buffer into the memory managed by the Orthanc core + void Assign(const void* buffer, + size_t size); +#endif + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + void Assign(const std::string& s); +#endif + // This transfers ownership from "other" to "this" void Assign(OrthancPluginMemoryBuffer& other); @@ -227,11 +238,11 @@ OrthancPluginMemoryBuffer Release(); - const char* GetData() const + const void* GetData() const { if (buffer_.size > 0) { - return reinterpret_cast<const char*>(buffer_.data); + return buffer_.data; } else { @@ -855,6 +866,13 @@ const HttpHeaders& headers) const; bool DoPost(MemoryBuffer& target, + size_t index, + const std::string& uri, + const std::string& body, + const HttpHeaders& headers, + unsigned int timeout) const; + + bool DoPost(MemoryBuffer& target, const std::string& name, const std::string& uri, const std::string& body, @@ -867,6 +885,13 @@ const HttpHeaders& headers) const; bool DoPost(Json::Value& target, + size_t index, + const std::string& uri, + const std::string& body, + const HttpHeaders& headers, + unsigned int timeout) const; + + bool DoPost(Json::Value& target, const std::string& name, const std::string& uri, const std::string& body, @@ -1589,10 +1614,14 @@ return requestBody_; } + // Execute only bool Execute(); + // Forward response as is + void ForwardAnswer(OrthancPluginContext* context, OrthancPluginRestOutput* output); + // Execute and forward the response as is - void Forward(OrthancPluginContext* context, OrthancPluginRestOutput* output); + void ExecuteAndForwardAnswer(OrthancPluginContext* context, OrthancPluginRestOutput* output); uint16_t GetHttpStatus() const; @@ -1604,4 +1633,101 @@ bool GetAnswerJson(Json::Value& output) const; }; #endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + class KeyValueStore : public boost::noncopyable + { + public: + class Iterator : public boost::noncopyable + { + private: + OrthancPluginKeysValuesIterator *iterator_; + + public: + Iterator(OrthancPluginKeysValuesIterator *iterator); + + ~Iterator(); + + bool Next(); + + std::string GetKey() const; + + void GetValue(std::string& target) const; + }; + + private: + std::string storeId_; + + public: + explicit KeyValueStore(const std::string& storeId) : + storeId_(storeId) + { + } + + const std::string& GetStoreId() const + { + return storeId_; + } + + void Store(const std::string& key, + const void* value, + size_t valueSize); + + void Store(const std::string& key, + const std::string& value) + { + Store(key, value.empty() ? NULL : value.c_str(), value.size()); + } + + bool GetValue(std::string& value, + const std::string& key); + + void DeleteKey(const std::string& key); + + Iterator* CreateIterator(); + }; +#endif + + +#if HAS_ORTHANC_PLUGIN_QUEUES == 1 + class Queue : public boost::noncopyable + { + private: + std::string queueId_; + + bool DequeueInternal(std::string& value, OrthancPluginQueueOrigin origin); + + public: + explicit Queue(const std::string& queueId) : + queueId_(queueId) + { + } + + const std::string& GetQueueId() const + { + return queueId_; + } + + void Enqueue(const void* value, + size_t valueSize); + + void Enqueue(const std::string& value) + { + Enqueue(value.empty() ? NULL : value.c_str(), value.size()); + } + + bool DequeueBack(std::string& value) + { + return DequeueInternal(value, OrthancPluginQueueOrigin_Back); + } + + bool DequeueFront(std::string& value) + { + return DequeueInternal(value, OrthancPluginQueueOrigin_Front); + } + + uint64_t GetSize(); + }; +#endif }