Mercurial > hg > orthanc-authorization
changeset 269:4272817fa3fd inbox
integration mainline->inbox
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Fri, 08 Aug 2025 17:27:01 +0200 |
parents | ac7d3395bb80 (diff) b50110b037cd (current diff) |
children | 18881ed67640 |
files | Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h |
diffstat | 11 files changed, 1098 insertions(+), 142 deletions(-) [+] |
line wrap: on
line diff
--- a/NEWS Fri Aug 08 17:26:31 2025 +0200 +++ b/NEWS Fri Aug 08 17:27:01 2025 +0200 @@ -1,3 +1,25 @@ +Pending changes in the mainline +=============================== + +* 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 + - Enabled by the "EnableAuditLogs" configuration. + - Audit-logs are currently handled by the PostgreSQL plugin and can be + browsed through the route /auth/audit-logs. + - New default permission "audit-logs" to grant access to the + "/auth/audit-logs" route. + + + 2025-07-14 - v 0.9.4 ====================
--- a/Plugin/AuthorizationWebService.cpp Fri Aug 08 17:26:31 2025 +0200 +++ b/Plugin/AuthorizationWebService.cpp Fri Aug 08 17:27:01 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,8 +373,86 @@ { 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(); + } } + bool AuthorizationWebService::GetUserProfileFromUserId(unsigned int& validity, + UserProfile& profile /* out */, + const std::string& userId) + { + if (userProfileUrl_.empty()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest, "Can not get user profile if the 'WebServiceUserProfileUrl' is not configured"); + } + + Json::Value body; + + body["user-id"] = userId; + + if (!identifier_.empty()) + { + body["identifier"] = identifier_; + } + else + { + body["identifier"] = Json::nullValue; + } + + std::string bodyAsString; + Orthanc::Toolbox::WriteFastJson(bodyAsString, body); + + try + { + HttpClient authClient; + authClient.SetUrl(userProfileUrl_); + if (!username_.empty()) + { + authClient.SetCredentials(username_, password_); + } + authClient.SetBody(bodyAsString); + authClient.SetMethod(OrthancPluginHttpMethod_Post); + authClient.AddHeader("Content-Type", "application/json"); + authClient.AddHeader("Expect", ""); + authClient.SetTimeout(10); + + Json::Value jsonProfile; + OrthancPlugins::HttpHeaders answerHeaders; + authClient.Execute(answerHeaders, jsonProfile); + + if (jsonProfile.isNull()) + { + validity = 60; + return false; + } + else if (!jsonProfile.isMember(VALIDITY) || + jsonProfile[VALIDITY].type() != Json::intValue) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, + "Syntax error in the result of the Auth Web service, the format of the UserProfile is invalid"); + } + validity = jsonProfile[VALIDITY].asUInt(); + + FromJson(profile, jsonProfile); + + return true; + } + catch (Orthanc::OrthancException& ex) + { + return false; + } + } bool AuthorizationWebService::GetUserProfileInternal(unsigned int& validity,
--- a/Plugin/AuthorizationWebService.h Fri Aug 08 17:26:31 2025 +0200 +++ b/Plugin/AuthorizationWebService.h Fri Aug 08 17:27:01 2025 +0200 @@ -111,5 +111,8 @@ static void FromJson(UserProfile& profile, const Json::Value& input); + virtual bool GetUserProfileFromUserId(unsigned int& validity, + UserProfile& profile /* out */, + const std::string& userId) ORTHANC_OVERRIDE; }; }
--- a/Plugin/BaseAuthorizationService.h Fri Aug 08 17:26:31 2025 +0200 +++ b/Plugin/BaseAuthorizationService.h Fri Aug 08 17:27:01 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/CachedAuthorizationService.cpp Fri Aug 08 17:26:31 2025 +0200 +++ b/Plugin/CachedAuthorizationService.cpp Fri Aug 08 17:27:01 2025 +0200 @@ -66,7 +66,8 @@ CachedAuthorizationService::CachedAuthorizationService(BaseAuthorizationService* decorated /* takes ownership */, ICacheFactory& factory) : decorated_(decorated), - cache_(factory.Create()) + cache_(factory.Create()), + cacheUserId_(factory.Create()) { if (decorated_.get() == NULL) { @@ -114,7 +115,61 @@ } } - + bool CachedAuthorizationService::GetUserProfileFromUserId(unsigned int& validityNotUsed, + UserProfile& profile /* out */, + const std::string& userId) + { + assert(decorated_.get() != NULL); + + std::string key = "user-id-" + userId; + std::string serializedProfile; + + if (cacheUserId_->Retrieve(serializedProfile, key)) + { + // Return the previously cached profile + Json::Value jsonProfile; + + Orthanc::Toolbox::ReadJson(jsonProfile, serializedProfile); + + AuthorizationWebService::FromJson(profile, jsonProfile); + + return true; + } + else + { + unsigned int validity; + + if (decorated_->GetUserProfileFromUserId(validity, profile, userId)) + { + Json::Value jsonProfile; + + AuthorizationWebService::ToJson(jsonProfile, profile); + Orthanc::Toolbox::WriteFastJson(serializedProfile, jsonProfile); + + cacheUserId_->Store(key, serializedProfile, validity); + + return true; + } + else // if no user was found, store it as a profile where the user name is the user id + { + validity = 60; + profile.userId = userId; + profile.name = userId; + + Json::Value jsonProfile; + + AuthorizationWebService::ToJson(jsonProfile, profile); + Orthanc::Toolbox::WriteFastJson(serializedProfile, jsonProfile); + + cacheUserId_->Store(key, serializedProfile, validity); + + return true; + } + } + + return false; + } + bool CachedAuthorizationService::GetUserProfileInternal(unsigned int& validityNotUsed, UserProfile& profile /* out */, const Token* token,
--- a/Plugin/CachedAuthorizationService.h Fri Aug 08 17:26:31 2025 +0200 +++ b/Plugin/CachedAuthorizationService.h Fri Aug 08 17:27:01 2025 +0200 @@ -37,6 +37,7 @@ private: std::unique_ptr<BaseAuthorizationService> decorated_; std::unique_ptr<ICache> cache_; + std::unique_ptr<ICache> cacheUserId_; std::string ComputeKey(OrthancPluginHttpMethod method, const AccessedResource& access, @@ -58,6 +59,10 @@ const Token* token, const std::string& tokenValue) ORTHANC_OVERRIDE; + virtual bool GetUserProfileFromUserId(unsigned int& validity /* out */, + UserProfile& profile /* out */, + const std::string& userId) ORTHANC_OVERRIDE; + virtual bool HasUserPermissionInternal(const std::string& permission, const UserProfile& profile) ORTHANC_OVERRIDE;
--- a/Plugin/DefaultConfiguration.json Fri Aug 08 17:26:31 2025 +0200 +++ b/Plugin/DefaultConfiguration.json Fri Aug 08 17:27:01 2025 +0200 @@ -128,8 +128,10 @@ // permission settings ["put", "^/auth/settings/roles$", "admin-permissions"], ["get", "^/auth/settings/roles$", "admin-permissions"], - ["get", "^/auth/settings/permissions$", "admin-permissions"] - + ["get", "^/auth/settings/permissions$", "admin-permissions"], + + // audit-logs + ["get", "^/auth/audit-logs$", "admin-permissions|audit-logs"] ] } } \ No newline at end of file
--- a/Plugin/Enumerations.cpp Fri Aug 08 17:26:31 2025 +0200 +++ b/Plugin/Enumerations.cpp Fri Aug 08 17:27:01 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 Fri Aug 08 17:26:31 2025 +0200 +++ b/Plugin/Enumerations.h Fri Aug 08 17:27:01 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 Fri Aug 08 17:26:31 2025 +0200 +++ b/Plugin/IAuthorizationService.h Fri Aug 08 17:27:01 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; @@ -89,14 +91,16 @@ const Token& token, const std::string& tokenValue) = 0; + virtual bool GetUserProfileFromUserId(unsigned int& validity /* out */, + UserProfile& profile /* out */, + const std::string& userId) = 0; + virtual bool GetAnonymousUserProfile(unsigned int& validity /* out */, UserProfile& profile /* out */) = 0; 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 Fri Aug 08 17:26:31 2025 +0200 +++ b/Plugin/Plugin.cpp Fri Aug 08 17:27:01 2025 +0200 @@ -31,6 +31,7 @@ #include <Toolbox.h> #include <SerializationToolbox.h> #include <EmbeddedResources.h> +#include "Enumerations.h" #define ORTHANC_PLUGIN_NAME "authorization" @@ -38,6 +39,7 @@ // Configuration of the authorization plugin static bool resourceTokensEnabled_ = false; static bool userTokensEnabled_ = false; +static bool enableAuditLogs_ = false; static std::unique_ptr<OrthancPlugins::IAuthorizationParser> authorizationParser_; static std::unique_ptr<OrthancPlugins::IAuthorizationService> authorizationService_; static std::unique_ptr<OrthancPlugins::PermissionParser> permissionParser_; @@ -65,6 +67,136 @@ } +static void MergeJson(Json::Value &a, + const Json::Value &b) +{ + // The semantics of this function is not generic enough to be included in the Orthanc framework + if (!a.isObject() || !b.isObject()) + { + return; + } + + Json::Value::Members members = b.getMemberNames(); + + for (size_t i = 0; i < members.size(); i++) + { + std::string key = members[i]; + + if (!a[key].isNull() && + a[key].type() == Json::objectValue && + b[key].type() == Json::objectValue) + { + MergeJson(a[key], b[key]); + } + else + { + a[key] = b[key]; + } + } +} + + +static const char* KEY_USER_DATA = "UserData"; +static const char* KEY_USER_ID = "AuditLogsUserId"; +static const char* KEY_PAYLOAD = "Payload"; +static const char* KEY_BEFORE_TAGS = "TagsBeforeModification"; + +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(); + + if (enableAuditLogs_) + { + // This function should not be called if audit logs are disabled + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 9) + // Audit logs are only available since Orthanc 1.12.9 + std::string serializedLogData; + const void* logDataPtr = NULL; + uint32_t logDataSize = 0; + + if (!logData.isNull()) + { + Orthanc::Toolbox::WriteFastJson(serializedLogData, logData); + logDataPtr = reinterpret_cast<const void*>(serializedLogData.c_str()); + logDataSize = serializedLogData.size(); + } + + OrthancPluginAuditLog(OrthancPlugins::GetGlobalContext(), + userId.c_str(), + resourceType, + resourceId.c_str(), + action.c_str(), + logDataPtr, + logDataSize); +#endif +} + +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 +405,88 @@ return false; } +static void RecordResourceAccessInternal(const OrthancPlugins::IAuthorizationService::UserProfile& profile, + const OrthancPlugins::IAuthorizationParser::AccessedResources& accesses, + const std::string& action, + const Json::Value& logData) +{ + for (OrthancPlugins::IAuthorizationParser::AccessedResources::const_iterator it = accesses.begin(); it != accesses.end(); ++it) + { + if (it->GetLevel() == OrthancPlugins::AccessLevel_Study) + { + RecordAuditLog(profile.userId, OrthancPluginResourceType_Study, it->GetOrthancId(), action, logData); + } + } +} + +static void RecordResourceAccess(const OrthancPlugins::IAuthorizationService::UserProfile& profile, + const std::string& uri, + OrthancPluginHttpMethod method, + const OrthancPlugins::AssociativeArray& getArguments) +{ + // Identify the resource + OrthancPlugins::IAuthorizationParser::AccessedResources accesses; + + if (authorizationParser_->Parse(accesses, uri, getArguments.GetMap())) + { + boost::smatch what; + + // Identify the action + boost::regex archive("^/(patients|studies|series|instances)/([a-f0-9-]+)/(archive|media)$"); + + if (boost::regex_match(uri, what, archive)) + { + RecordResourceAccessInternal(profile, accesses, "download", Json::nullValue); + } + } + +} + +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"; + + if (enableAuditLogs_) + { + RecordResourceAccess(profile, uri, method, getArguments); + } + } + 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 +527,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 +538,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 +559,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 +675,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 +786,62 @@ } } +bool GetUserNameFromUserId(std::string& userName, + const std::string& userId) +{ + unsigned int validity; // not used + OrthancPlugins::IAuthorizationService::UserProfile profile; + + if (authorizationService_->GetUserProfileFromUserId(validity, profile, userId)) + { + userName = profile.name; + return true; + } + + return false; +} + +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 +852,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 +1209,461 @@ 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())) + { + Json::Value logData; + logData[KEY_PAYLOAD] = payload; + + // 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(logData, userId); + + if (isModification) + { + // log the tags before modification (but not for anonymizations) + Json::Value resourceBefore; + if (resourceType == OrthancPluginResourceType_Study && OrthancPlugins::RestApiGet(resourceBefore, "/studies/" + resourceId, false)) + { + Json::Value studyTagsBefore = resourceBefore["MainDicomTags"]; + Json::Value patientTagsBefore = resourceBefore["PatientMainDicomTags"]; + MergeJson(studyTagsBefore, patientTagsBefore); + + logData[KEY_BEFORE_TAGS] = studyTagsBefore; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, "Auth plugin: TODO: unable to handle anonymize/modify other levels than studies with audit logs enabled."); + } + } + + // 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"), + logData); + + 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 BulkModifyAnonymizeWithAuditLogs(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, "Auth plugin: Not implemented: Currently unable to perform bulk modification/anonymization with audit logs enabled."); +} + + +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 (!enableAuditLogs_ || request->method == OrthancPluginHttpMethod_Get) + { + coreApi.ExecuteAndForwardAnswer(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; + } + } +} + + +void GetAuditLogs(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + OrthancPluginContext* context = OrthancPlugins::GetGlobalContext(); + + bool isOutputCsv = false; + + OrthancPlugins::HttpHeaders requestHeaders; + OrthancPlugins::GetHttpHeaders(requestHeaders, request); + + OrthancPlugins::GetArguments getArguments; + OrthancPlugins::GetGetArguments(getArguments, request); + + if (getArguments.find("format") != getArguments.end()) + { + isOutputCsv = getArguments["format"] == "csv"; + } + + if (!isOutputCsv && (requestHeaders.find("accept") != requestHeaders.end())) + { + std::string acceptHeader = requestHeaders["accept"]; + Orthanc::Toolbox::ToLowerCase(acceptHeader); + + isOutputCsv = acceptHeader.find("text/csv") != std::string::npos; + } + + OrthancPlugins::RestApiClient coreApi("/plugins/postgresql/audit-logs", request); + coreApi.SetAfterPlugins(true); + coreApi.SetRequestHeader("Accept", "application/json"); // the postgresql plugin only knows about the json format + + if (request->method != OrthancPluginHttpMethod_Get) + { + OrthancPluginSendMethodNotAllowed(context, output, "GET"); + return; + } + + Json::Value response; + + if (coreApi.Execute() && coreApi.GetAnswerJson(response)) + { + // transform the response: replace user-id by user-name + for (Json::ArrayIndex i = 0; i < response.size(); ++i) + { + const std::string& userId = response[i]["UserId"].asString(); + std::string userName; + if (GetUserNameFromUserId(userName, userId)) + { + response[i]["UserName"] = userName; + } + else + { + response[i]["UserName"] = userId; + } + } + + if (!isOutputCsv) + { + OrthancPlugins::AnswerJson(response, output); + } + else + { + std::vector<std::string> lines; + + std::vector<std::string> firstLineColumns; + firstLineColumns.push_back("Timestamp"); + firstLineColumns.push_back("UserId"); + firstLineColumns.push_back("UserName"); + firstLineColumns.push_back("ResourceId"); + firstLineColumns.push_back("Action"); + firstLineColumns.push_back("LogData"); + + std::string firstLine; + Orthanc::Toolbox::JoinStrings(firstLine, firstLineColumns,";"); + lines.push_back(firstLine); + + for (Json::ArrayIndex i = 0; i < response.size(); ++i) + { + std::vector<std::string> lineColumns; + std::string line; + + const Json::Value& log = response[i]; + lineColumns.push_back(log["Timestamp"].asString()); + lineColumns.push_back(log["UserId"].asString()); + lineColumns.push_back(log["UserName"].asString()); + lineColumns.push_back(log["ResourceId"].asString()); + lineColumns.push_back(log["Action"].asString()); + + std::string logData; + Orthanc::Toolbox::WriteFastJson(logData, log["LogData"]); + boost::replace_all(logData, "\n", ""); + lineColumns.push_back(logData); + + Orthanc::Toolbox::JoinStrings(line, lineColumns,";"); + lines.push_back(line); + } + + std::string csv; + Orthanc::Toolbox::JoinStrings(csv, lines, "\n"); + + OrthancPluginSetHttpHeader(context, output, "Content-disposition", "filename=\"audit-logs.csv\""); + OrthancPlugins::AnswerString(csv, "text/csv", output); + } + } +} + + +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) @@ -1020,6 +1763,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 +1973,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 +1983,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); } @@ -1299,31 +2067,6 @@ } -void MergeJson(Json::Value &a, const Json::Value &b) { - - if (!a.isObject() || !b.isObject()) - { - return; - } - - Json::Value::Members members = b.getMemberNames(); - - for (size_t i = 0; i < members.size(); i++) - { - std::string key = members[i]; - - if (!a[key].isNull() && a[key].type() == Json::objectValue && b[key].type() == Json::objectValue) - { - MergeJson(a[key], b[key]); - } - else - { - a[key] = b[key]; - } - } -} - - extern "C" { ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context) @@ -1425,6 +2168,9 @@ } #endif + enableAuditLogs_ = pluginConfiguration.GetBooleanValue("EnableAuditLogs", false); + + pluginConfiguration.LookupSetOfStrings(uncheckedResources_, "UncheckedResources", false); pluginConfiguration.LookupListOfStrings(uncheckedFolders_, "UncheckedFolders", false); @@ -1494,6 +2240,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 +2415,7 @@ (new OrthancPlugins::CachedAuthorizationService (webService.release(), factory)); - if (!urlTokenValidation.empty()) + if (!urlTokenValidation.empty() || enableAuditLogs_) { OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback); } @@ -1682,10 +2434,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 +2454,27 @@ 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); + OrthancPlugins::RegisterRestCallback<BulkModifyAnonymizeWithAuditLogs>("/tools/bulk-modify", true); + OrthancPlugins::RegisterRestCallback<BulkModifyAnonymizeWithAuditLogs>("/tools/bulk-anonymize", true); + OrthancPlugins::RegisterRestCallback<GetAuditLogs>("/auth/audit-logs", true); + + // Note: other "actions" that do not modify the data like download-archive are logged in the HTTP filter (see RecordResourceAccess()) + + // TODO + // /modalities/move + // /modalities/store + // /archive + create-archive + // /media + create-media + create-media-extended + + } } if (!urlTokenCreationBase.empty())