Mercurial > hg > orthanc-authorization
changeset 247:ff21632f3ab6 inbox
wip: audit-logs
author | Alain Mazy <am@orthanc.team> |
---|---|
date | Fri, 11 Jul 2025 15:38:38 +0200 |
parents | 26ca67fe0659 |
children | 2b09f8e98cfe |
files | Plugin/Plugin.cpp Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h |
diffstat | 3 files changed, 311 insertions(+), 14 deletions(-) [+] |
line wrap: on
line diff
--- a/Plugin/Plugin.cpp Mon Jul 07 10:55:00 2025 +0200 +++ b/Plugin/Plugin.cpp Fri Jul 11 15:38:38 2025 +0200 @@ -88,6 +88,28 @@ 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, @@ -97,6 +119,23 @@ 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 { @@ -386,6 +425,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) @@ -401,6 +441,7 @@ OrthancPlugins::IAuthorizationService::UserProfile anonymousProfile; unsigned int validityNotUsed; authorizationService_->GetUserProfile(validityNotUsed, anonymousProfile, OrthancPlugins::Token(OrthancPlugins::TokenType_None, ""), ""); + userId = "anonymous"; LOG(INFO) << msg; if (!TestRequiredPermissions(hasUserRequiredPermissions, requiredPermissions, anonymousProfile, msg, uri, method, getArguments)) @@ -421,6 +462,11 @@ unsigned int validityNotUsed; authorizationService_->GetUserProfile(validityNotUsed, profile, authTokens[i].GetToken(), authTokens[i].GetValue()); + if (!profile.userId.empty()) + { + userId = profile.userId; + } + if (!TestRequiredPermissions(hasUserRequiredPermissions, requiredPermissions, profile, msg, uri, method, getArguments)) { return 0; // the labels for this resource prevents access -> stop checking now ! @@ -1054,7 +1100,7 @@ // always forward to core OrthancPlugins::RestApiClient coreApi(url, request); - coreApi.Forward(context, output); + coreApi.ExecuteAndForwardAnswer(context, output); if (request->method == OrthancPluginHttpMethod_Post) { @@ -1063,7 +1109,7 @@ if (GetUserProfileInternal(profile, request) && coreApi.GetAnswerJson(coreResponse)) { - RecordAuditLog(profile.userId, OrthancPluginResourceType_Study, coreResponse["ParentStudy"].asString(), "uploaded-instance", std::string()); + RecordAuditLog(profile.userId, OrthancPluginResourceType_Study, coreResponse["ParentStudy"].asString(), "uploaded-instance", coreResponse["ID"].asString()); } else { @@ -1094,7 +1140,7 @@ OrthancPlugins::IAuthorizationService::UserProfile profile; std::string userId; - LOG(WARNING) << payload.toStyledString(); + // LOG(WARNING) << payload.toStyledString(); if (GetUserProfileInternal(profile, request) && !profile.userId.empty()) { @@ -1118,14 +1164,20 @@ (isModification ? "start-modification-job" : "start-anonymization-job"), payload); - coreApi.Forward(context, output); + if (coreApi.Execute()) + { + coreApi.ForwardAnswer(context, output); + } } else { Json::Value coreResponse; // if it is synchronous, perform the modification and record the log directly - coreApi.Forward(context, output); + if (coreApi.Execute()) + { + coreApi.ForwardAnswer(context, output); + } if (coreApi.GetAnswerJson(coreResponse)) { @@ -1148,6 +1200,216 @@ 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*/, @@ -1218,7 +1480,7 @@ if (request->method != OrthancPluginHttpMethod_Get) { OrthancPlugins::RestApiClient coreApi(url, request); - coreApi.Forward(context, output); + coreApi.ExecuteAndForwardAnswer(context, output); } else { @@ -1248,6 +1510,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) @@ -1927,10 +2203,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); @@ -1955,6 +2229,17 @@ 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 + } }
--- a/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp Mon Jul 07 10:55:00 2025 +0200 +++ b/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp Fri Jul 11 15:38:38 2025 +0200 @@ -4284,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)
--- a/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h Mon Jul 07 10:55:00 2025 +0200 +++ b/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h Fri Jul 11 15:38:38 2025 +0200 @@ -1614,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;