# HG changeset patch # User Alain Mazy # Date 1752252107 -7200 # Node ID 2b09f8e98cfe91d6ccb1958f9877f3024b220a20 # Parent ff21632f3ab6b0924a9bc75b544d6f13030ad13c# Parent d18a5deb19cf2579403de94144b01086db74da96 merge default -> inbox diff -r d18a5deb19cf -r 2b09f8e98cfe NEWS --- a/NEWS Fri Jul 11 18:20:32 2025 +0200 +++ b/NEWS Fri Jul 11 18:41:47 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. diff -r d18a5deb19cf -r 2b09f8e98cfe Plugin/AuthorizationWebService.cpp --- a/Plugin/AuthorizationWebService.cpp Fri Jul 11 18:20:32 2025 +0200 +++ b/Plugin/AuthorizationWebService.cpp Fri Jul 11 18:41:47 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(); + } } diff -r d18a5deb19cf -r 2b09f8e98cfe Plugin/BaseAuthorizationService.h --- a/Plugin/BaseAuthorizationService.h Fri Jul 11 18:20:32 2025 +0200 +++ b/Plugin/BaseAuthorizationService.h Fri Jul 11 18:41:47 2025 +0200 @@ -100,24 +100,5 @@ return false; } - virtual bool HasAnonymousUserPermission(const std::set& anyOfPermissions) ORTHANC_OVERRIDE - { - if (anyOfPermissions.size() == 0) - { - return true; - } - - UserProfile anonymousUserProfile; - anonymousUserProfile.tokenType = TokenType_None; - - for (std::set::const_iterator it = anyOfPermissions.begin(); it != anyOfPermissions.end(); ++it) - { - if (HasUserPermissionInternal(*it, anonymousUserProfile)) - { - return true; - } - } - return false; - } }; } diff -r d18a5deb19cf -r 2b09f8e98cfe Plugin/Enumerations.cpp --- a/Plugin/Enumerations.cpp Fri Jul 11 18:20:32 2025 +0200 +++ b/Plugin/Enumerations.cpp Fri Jul 11 18:41:47 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 + "'"); + } + } diff -r d18a5deb19cf -r 2b09f8e98cfe Plugin/Enumerations.h --- a/Plugin/Enumerations.h Fri Jul 11 18:20:32 2025 +0200 +++ b/Plugin/Enumerations.h Fri Jul 11 18:41:47 2025 +0200 @@ -21,6 +21,7 @@ #pragma once #include +#include namespace OrthancPlugins { @@ -46,4 +47,6 @@ std::string EnumerationToString(AccessLevel level); AccessLevel StringToAccessLevel(const std::string& level); + + OrthancPluginResourceType StringToResourceType(const char* type); } diff -r d18a5deb19cf -r 2b09f8e98cfe Plugin/IAuthorizationService.h --- a/Plugin/IAuthorizationService.h Fri Jul 11 18:20:32 2025 +0200 +++ b/Plugin/IAuthorizationService.h Fri Jul 11 18:41:47 2025 +0200 @@ -61,8 +61,10 @@ struct UserProfile { std::string name; + std::string userId; std::set permissions; std::set authorizedLabels; + std::set groups; // the source token key/value that identified the user TokenType tokenType; @@ -95,8 +97,6 @@ virtual bool HasUserPermission(const std::set& anyOfPermissions, const UserProfile& profile) = 0; - virtual bool HasAnonymousUserPermission(const std::set& anyOfPermissions) = 0; - virtual bool CreateToken(CreatedToken& response, const std::string& tokenType, const std::string& id, diff -r d18a5deb19cf -r 2b09f8e98cfe Plugin/Plugin.cpp --- a/Plugin/Plugin.cpp Fri Jul 11 18:20:32 2025 +0200 +++ b/Plugin/Plugin.cpp Fri Jul 11 18:41:47 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 authorizationParser_; static std::unique_ptr authorizationService_; static std::unique_ptr 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& auditLogs) +{ + for (std::list::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& 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::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::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& 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 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 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::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::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("/auth/settings/roles", true); OrthancPlugins::RegisterRestCallback("/auth/settings/permissions", true); - OrthancPlugins::RegisterRestCallback("/instances/([^/]*)", true); - OrthancPlugins::RegisterRestCallback("/series/([^/]*)", true); - OrthancPlugins::RegisterRestCallback("/studies/([^/]*)", true); - OrthancPlugins::RegisterRestCallback("/patients/([^/]*)", true); + OrthancPlugins::RegisterRestCallback("/(patients|studies|series)/([^/]*)", true); // this includes auditLogs + OrthancPlugins::RegisterRestCallback("/instances/([^/]*)/patient", true); OrthancPlugins::RegisterRestCallback("/instances/([^/]*)/study", true); OrthancPlugins::RegisterRestCallback("/instances/([^/]*)/series", true); @@ -1704,6 +2222,24 @@ OrthancPlugins::RegisterRestCallback("/patients/([^/]*)/instances", true); OrthancPlugins::RegisterRestCallback("/patients/([^/]*)/series", true); OrthancPlugins::RegisterRestCallback("/patients/([^/]*)/studies", true); + + if (enableAuditLogs_) + { + OrthancPlugins::RegisterRestCallback("/instances", true); + OrthancPlugins::RegisterRestCallback("/(patients|studies|series)/([^/]*)/anonymize", true); + OrthancPlugins::RegisterRestCallback("/(patients|studies|series)/([^/]*)/modify", true); + OrthancPlugins::RegisterRestCallback("/(patients|studies|series)/([^/]*)/labels/([^/]*)", true); + OrthancPlugins::RegisterRestCallback("/tools/bulk-delete", true); + + // TODO + // OrthancPlugins::RegisterRestCallback("/tools/bulk-modify", true); + // /modalities/move + // /modalities/store + // /archive + create-archive + // /media + create-media + create-media-extended + // /bulk-anonymize + bulkd-modify + + } } if (!urlTokenCreationBase.empty()) diff -r d18a5deb19cf -r 2b09f8e98cfe Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp --- a/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp Fri Jul 11 18:20:32 2025 +0200 +++ b/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp Fri Jul 11 18:41:47 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(size); - if (static_cast(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(size); + if (static_cast(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(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(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(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(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(static_cast(valueSize)) != valueSize) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory); + } + + OrthancPluginErrorCode code = OrthancPluginStoreKeyValue(OrthancPlugins::GetGlobalContext(), storeId_.c_str(), + key.c_str(), value, static_cast(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(static_cast(valueSize)) != valueSize) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory); + } + + OrthancPluginErrorCode code = OrthancPluginEnqueueValue(OrthancPlugins::GetGlobalContext(), + queueId_.c_str(), value, static_cast(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 } diff -r d18a5deb19cf -r 2b09f8e98cfe Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h --- a/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h Fri Jul 11 18:20:32 2025 +0200 +++ b/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h Fri Jul 11 18:41:47 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(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 }