changeset 259:a2f7066fd932 inbox

audit logs continued
author Alain Mazy <am@orthanc.team>
date Thu, 24 Jul 2025 17:59:45 +0200
parents 3f2a3192594a
children 95b22ee07bc3
files NEWS Plugin/AuthorizationWebService.cpp Plugin/AuthorizationWebService.h Plugin/CachedAuthorizationService.cpp Plugin/CachedAuthorizationService.h Plugin/DefaultConfiguration.json Plugin/IAuthorizationService.h Plugin/Plugin.cpp
diffstat 8 files changed, 207 insertions(+), 5 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Tue Jul 22 17:25:07 2025 +0200
+++ b/NEWS	Thu Jul 24 17:59:45 2025 +0200
@@ -14,9 +14,9 @@
 * 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 /plugins/postgresql/audit-logs.
+    browsed through the route /auth/audit-logs.
   - New default permission "audit-logs" to grant access to the 
-    "/plugins/postgresql/audit-logs" route.
+    "/auth/audit-logs" route.
 
 
 
--- a/Plugin/AuthorizationWebService.cpp	Tue Jul 22 17:25:07 2025 +0200
+++ b/Plugin/AuthorizationWebService.cpp	Thu Jul 24 17:59:45 2025 +0200
@@ -388,6 +388,71 @@
     }
   }
 
+  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	Tue Jul 22 17:25:07 2025 +0200
+++ b/Plugin/AuthorizationWebService.h	Thu Jul 24 17:59:45 2025 +0200
@@ -110,5 +110,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/CachedAuthorizationService.cpp	Tue Jul 22 17:25:07 2025 +0200
+++ b/Plugin/CachedAuthorizationService.cpp	Thu Jul 24 17:59:45 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	Tue Jul 22 17:25:07 2025 +0200
+++ b/Plugin/CachedAuthorizationService.h	Thu Jul 24 17:59:45 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	Tue Jul 22 17:25:07 2025 +0200
+++ b/Plugin/DefaultConfiguration.json	Thu Jul 24 17:59:45 2025 +0200
@@ -131,7 +131,7 @@
             ["get", "^/auth/settings/permissions$", "admin-permissions"],
 
             // audit-logs
-            ["get", "^/plugins/postgresql/audit-logs$", "admin-permissions|audit-logs"]
+            ["get", "^/auth/audit-logs$", "admin-permissions|audit-logs"]
         ]
     }
 }
\ No newline at end of file
--- a/Plugin/IAuthorizationService.h	Tue Jul 22 17:25:07 2025 +0200
+++ b/Plugin/IAuthorizationService.h	Thu Jul 24 17:59:45 2025 +0200
@@ -91,6 +91,10 @@
                                 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;
 
--- a/Plugin/Plugin.cpp	Tue Jul 22 17:25:07 2025 +0200
+++ b/Plugin/Plugin.cpp	Thu Jul 24 17:59:45 2025 +0200
@@ -746,6 +746,21 @@
   }
 }
 
+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,
@@ -1346,6 +1361,60 @@
 }
 
 
+void GetAuditLogs(OrthancPluginRestOutput* output,
+                  const char* url,
+                  const OrthancPluginHttpRequest* request)
+{
+  OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
+
+  OrthancPlugins::RestApiClient coreApi("/plugins/postgresql/audit-logs", request);
+  coreApi.SetAfterPlugins(true);
+
+  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;
+      }
+    }
+
+    OrthancPlugins::AnswerJson(response, output);
+  }
+
+  // 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.");
+  //   }
+    
+}
+
+
 OrthancPluginResourceType IdentifyResourceType(const std::string& resourceId)
 {
   Json::Value v;
@@ -2302,6 +2371,7 @@
             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())