changeset 6246:d70e4de0c847

OrthancPluginRecordAuditLog
author Alain Mazy <am@orthanc.team>
date Mon, 14 Jul 2025 16:10:24 +0200
parents 93d2e634fc6d
children 02c3f861b6e6 a8c0be03dae3
files NEWS OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp OrthancServer/Plugins/Engine/OrthancPlugins.cpp OrthancServer/Plugins/Engine/OrthancPlugins.h OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto OrthancServer/Sources/Database/IDatabaseWrapper.h OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp OrthancServer/Sources/Database/StatelessDatabaseOperations.h OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp
diffstat 13 files changed, 250 insertions(+), 2 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Fri Jul 11 17:33:28 2025 +0200
+++ b/NEWS	Mon Jul 14 16:10:24 2025 +0200
@@ -32,6 +32,8 @@
     DICOM resource from a plugin.
   - "OrthancPluginRegisterHttpAuthentication()" to install a custom
     callback to authenticate HTTP requests.
+  - "OrthancPluginRecordAuditLog()" to record an audit log in DB, provided
+    that the DB plugin supports it.
 * The OrthancPluginHttpRequest structure provides the payload of
   the possible HTTP authentication callback.
 * OrthancPluginCallRestApi() now also returns the body of DELETE requests:
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp	Fri Jul 11 17:33:28 2025 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp	Mon Jul 14 16:10:24 2025 +0200
@@ -1511,6 +1511,16 @@
     {
       throw OrthancException(ErrorCode_NotImplemented);  // Not supported
     }
+
+    virtual void RecordAuditLog(const std::string& userId,
+                                ResourceType resourceType,
+                                const std::string& resourceId,
+                                const std::string& action,
+                                const void* logData,
+                                size_t logDataSize) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_NotImplemented);  // Not supported
+    }
   };
 
 
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp	Fri Jul 11 17:33:28 2025 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp	Mon Jul 14 16:10:24 2025 +0200
@@ -1123,6 +1123,17 @@
     {
       throw OrthancException(ErrorCode_NotImplemented);  // Not supported
     }
+
+    virtual void RecordAuditLog(const std::string& userId,
+                                ResourceType resourceType,
+                                const std::string& resourceId,
+                                const std::string& action,
+                                const void* logData,
+                                size_t logDataSize) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_NotImplemented);  // Not supported
+    }
+
   };
 
   
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp	Fri Jul 11 17:33:28 2025 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp	Mon Jul 14 16:10:24 2025 +0200
@@ -2051,6 +2051,39 @@
         throw OrthancException(ErrorCode_InternalError);
       }
     }
+
+    virtual void RecordAuditLog(const std::string& userId,
+                                ResourceType resourceType,
+                                const std::string& resourceId,
+                                const std::string& action,
+                                const void* logData,
+                                size_t logDataSize) ORTHANC_OVERRIDE
+    {
+      // In protobuf, bytes "may contain any arbitrary sequence of bytes no longer than 2^32"
+      // https://protobuf.dev/programming-guides/proto3/
+      if (logDataSize > std::numeric_limits<uint32_t>::max())
+      {
+        throw OrthancException(ErrorCode_NotEnoughMemory);
+      }
+
+      if (database_.GetDatabaseCapabilities().HasAuditLogsSupport())
+      {
+        DatabasePluginMessages::TransactionRequest request;
+        request.mutable_record_audit_log()->set_user_id(userId);
+        request.mutable_record_audit_log()->set_resource_type(Convert(resourceType));
+        request.mutable_record_audit_log()->set_resource_id(resourceId);
+        request.mutable_record_audit_log()->set_action(action);
+        request.mutable_record_audit_log()->set_log_data(logData, logDataSize);
+
+        ExecuteTransaction(DatabasePluginMessages::OPERATION_ENQUEUE_VALUE, request);
+      }
+      else
+      {
+        // This method shouldn't have been called
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
   };
 
 
@@ -2144,6 +2177,7 @@
       dbCapabilities_.SetKeyValueStoresSupport(systemInfo.supports_key_value_stores());
       dbCapabilities_.SetQueuesSupport(systemInfo.supports_queues());
       dbCapabilities_.SetAttachmentCustomDataSupport(systemInfo.has_attachment_custom_data());
+      dbCapabilities_.SetAuditLogsSupport(systemInfo.supports_audit_logs());
     }
 
     open_ = true;
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Fri Jul 11 17:33:28 2025 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Mon Jul 14 16:10:24 2025 +0200
@@ -4768,6 +4768,18 @@
     }
   }
 
+  void OrthancPlugins::ApplyRecordAuditLog(const _OrthancPluginRecordAuditLog& parameters)
+  {
+    PImpl::ServerContextReference lock(*pimpl_);
+
+    if (!lock.GetContext().GetIndex().HasAuditLogsSupport())
+    {
+      throw OrthancException(ErrorCode_NotImplemented, "The database engine does not support audit logs");
+    }
+
+    lock.GetContext().GetIndex().RecordAuditLog(parameters.userId, Plugins::Convert(parameters.resourceType), parameters.resourceId, parameters.action, parameters.logData, parameters.logDataSize);
+  }
+
   void OrthancPlugins::ApplyLoadDicomInstance(const _OrthancPluginLoadDicomInstance& params)
   {
     std::unique_ptr<IDicomInstance> target;
@@ -5898,6 +5910,13 @@
         return true;
       }
 
+      case _OrthancPluginService_RecordAuditLog:
+      {
+        const _OrthancPluginRecordAuditLog& p = *reinterpret_cast<const _OrthancPluginRecordAuditLog*>(parameters);
+        ApplyRecordAuditLog(p);
+        return true;
+      }
+
       default:
         return false;
     }
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.h	Fri Jul 11 17:33:28 2025 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.h	Mon Jul 14 16:10:24 2025 +0200
@@ -249,6 +249,8 @@
 
     void ApplySetStableStatus(const _OrthancPluginSetStableStatus& parameters);
 
+    void ApplyRecordAuditLog(const _OrthancPluginRecordAuditLog& parameters);
+
     void ComputeHash(_OrthancPluginService service,
                      const void* parameters);
 
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Fri Jul 11 17:33:28 2025 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Mon Jul 14 16:10:24 2025 +0200
@@ -502,6 +502,7 @@
     _OrthancPluginService_DequeueValue = 58,                        /* New in Orthanc 1.12.8 */
     _OrthancPluginService_GetQueueSize = 59,                        /* New in Orthanc 1.12.8 */
     _OrthancPluginService_SetStableStatus = 60,                     /* New in Orthanc 1.12.9 */
+    _OrthancPluginService_RecordAuditLog = 61,                      /* New in Orthanc 1.12.9 */
 
     /* Registration of callbacks */
     _OrthancPluginService_RegisterRestCallback = 1000,
@@ -10470,6 +10471,50 @@
   }
 
 
+  typedef struct
+  {
+    const char*               userId;
+    OrthancPluginResourceType resourceType;
+    const char*               resourceId;
+    const char*               action;
+    const void*               logData;
+    uint32_t                  logDataSize;
+  } _OrthancPluginRecordAuditLog;
+
+
+  /**
+   * @brief Record an audit log
+   *
+   * Record an audit log (provided that a database plugin provides the feature).
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param userId A string uniquely identifying the user or entity that is executing the action on the resource.
+   * @param resourceType The type of the resource this log relates to.
+   * @param resourceId The resource this log relates to.
+   * @param action The action that is performed on the resource.
+   * @param logData A pointer to custom log data.
+   * @param logDataSize The size of custom log data.
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginRecordAuditLog(
+    OrthancPluginContext* context,
+    const char*               userId,
+    OrthancPluginResourceType resourceType,
+    const char*               resourceId,
+    const char*               action,
+    const void*               logData,
+    uint32_t                  logDataSize)
+  {
+    _OrthancPluginRecordAuditLog m;
+    m.userId = userId;
+    m.resourceType = resourceType;
+    m.resourceId = resourceId;
+    m.action = action;
+    m.logData = logData;
+    m.logDataSize = logDataSize;
+    context->InvokeService(context, _OrthancPluginService_RecordAuditLog, &m);
+  }
+
+
 #ifdef  __cplusplus
 }
 #endif
--- a/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto	Fri Jul 11 17:33:28 2025 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto	Mon Jul 14 16:10:24 2025 +0200
@@ -175,6 +175,7 @@
     bool supports_key_value_stores = 10;  // New in Orthanc 1.12.8
     bool supports_queues = 11;            // New in Orthanc 1.12.8
     bool has_attachment_custom_data = 12; // New in Orthanc 1.12.8
+    bool supports_audit_logs = 13;            // New in Orthanc 1.12.9
   }
 }
 
@@ -339,6 +340,7 @@
   OPERATION_GET_QUEUE_SIZE = 59;              // New in Orthanc 1.12.8
   OPERATION_GET_ATTACHMENT_CUSTOM_DATA = 60;  // New in Orthanc 1.12.8
   OPERATION_SET_ATTACHMENT_CUSTOM_DATA = 61;  // New in Orthanc 1.12.8
+  OPERATION_RECORD_AUDIT_LOG = 62;            // New in Orthanc 1.12.9
 
 }
 
@@ -1095,6 +1097,20 @@
   }
 }
 
+message RecordAuditLog {
+  message Request {
+    string user_id = 1;
+    ResourceType  resource_type = 2;
+    string resource_id = 3;
+    string action = 4;
+    bytes log_data = 5;
+  }
+
+  message Response {
+  }
+}
+
+
 message TransactionRequest {
   sfixed64              transaction = 1;
   TransactionOperation  operation = 2;
@@ -1161,6 +1177,7 @@
   GetQueueSize.Request                    get_queue_size = 159;
   GetAttachmentCustomData.Request         get_attachment_custom_data = 160;
   SetAttachmentCustomData.Request         set_attachment_custom_data = 161;
+  RecordAuditLog.Request                  record_audit_log = 162;
 }
 
 message TransactionResponse {
@@ -1226,6 +1243,7 @@
   GetQueueSize.Response                    get_queue_size = 159;
   GetAttachmentCustomData.Response         get_attachment_custom_data = 160;
   SetAttachmentCustomData.Response         set_attachment_custom_data = 161;
+  RecordAuditLog.Response                  record_audit_log = 162;
 }
 
 enum RequestType {
--- a/OrthancServer/Sources/Database/IDatabaseWrapper.h	Fri Jul 11 17:33:28 2025 +0200
+++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h	Mon Jul 14 16:10:24 2025 +0200
@@ -59,6 +59,7 @@
       bool hasAttachmentCustomDataSupport_;
       bool hasKeyValueStoresSupport_;
       bool hasQueuesSupport_;
+      bool hasAuditLogsSupport_;
 
     public:
       Capabilities() :
@@ -72,7 +73,8 @@
         hasExtendedChanges_(false),
         hasAttachmentCustomDataSupport_(false),
         hasKeyValueStoresSupport_(false),
-        hasQueuesSupport_(false)
+        hasQueuesSupport_(false),
+        hasAuditLogsSupport_(false)
       {
       }
 
@@ -185,6 +187,17 @@
       {
         return hasQueuesSupport_;
       }
+
+      void SetAuditLogsSupport(bool value)
+      {
+        hasAuditLogsSupport_ = value;
+      }
+
+      bool HasAuditLogsSupport() const
+      {
+        return hasAuditLogsSupport_;
+      }
+
     };
 
 
@@ -469,6 +482,15 @@
 
       // New in Orthanc 1.12.8, for statistics only
       virtual uint64_t GetQueueSize(const std::string& queueId) = 0;
+
+      // New in Orthanc 1.12.9
+      virtual void RecordAuditLog(const std::string& userId,
+                                  ResourceType resourceType,
+                                  const std::string& resourceId,
+                                  const std::string& action,
+                                  const void* logData,
+                                  size_t logDataSize) = 0;
+
     };
 
 
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Fri Jul 11 17:33:28 2025 +0200
+++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Mon Jul 14 16:10:24 2025 +0200
@@ -2340,6 +2340,17 @@
       s.Step();
       return s.ColumnInt64(0);
     }
+
+    virtual void RecordAuditLog(const std::string& userId,
+                                ResourceType resourceType,
+                                const std::string& resourceId,
+                                const std::string& action,
+                                const void* logData,
+                                size_t logDataSize) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_NotImplemented);  // Not supported
+    }
+
   };
 
 
@@ -2587,6 +2598,7 @@
     dbCapabilities_.SetKeyValueStoresSupport(true);
     dbCapabilities_.SetQueuesSupport(true);
     dbCapabilities_.SetAttachmentCustomDataSupport(true);
+    dbCapabilities_.SetAuditLogsSupport(false);
     db_.Open(path);
   }
 
@@ -2604,6 +2616,7 @@
     dbCapabilities_.SetKeyValueStoresSupport(true);
     dbCapabilities_.SetQueuesSupport(true);
     dbCapabilities_.SetAttachmentCustomDataSupport(true);
+    dbCapabilities_.SetAuditLogsSupport(false);
     db_.OpenInMemory();
   }
 
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Fri Jul 11 17:33:28 2025 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Mon Jul 14 16:10:24 2025 +0200
@@ -3226,6 +3226,12 @@
     return db_.GetDatabaseCapabilities().HasQueuesSupport();
   }
 
+  bool StatelessDatabaseOperations::HasAuditLogsSupport()
+  {
+    boost::shared_lock<boost::shared_mutex> lock(mutex_);
+    return db_.GetDatabaseCapabilities().HasAuditLogsSupport();
+  }
+
   void StatelessDatabaseOperations::ExecuteCount(uint64_t& count,
                                                  const FindRequest& request)
   {
@@ -3755,4 +3761,48 @@
       throw OrthancException(ErrorCode_BadSequenceOfCalls);
     }
   }
+
+  void StatelessDatabaseOperations::RecordAuditLog(const std::string& userId,
+                                                   ResourceType resourceType,
+                                                   const std::string& resourceId,
+                                                   const std::string& action,
+                                                   const void* logData,
+                                                   size_t logDataSize)
+  {
+    class Operations : public IReadWriteOperations
+    {
+    private:
+      const std::string& userId_;
+      ResourceType resourceType_;
+      const std::string& resourceId_;
+      const std::string& action_;
+      const void* logData_;
+      size_t logDataSize_;
+
+    public:
+      Operations(const std::string& userId,
+                 ResourceType resourceType,
+                 const std::string& resourceId,
+                 const std::string& action,
+                 const void* logData,
+                 size_t logDataSize) :
+        userId_(userId),
+        resourceType_(resourceType),
+        resourceId_(resourceId),
+        action_(action),
+        logData_(logData),
+        logDataSize_(logDataSize)
+      {
+      }
+
+      virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
+      {
+        transaction.RecordAuditLog(userId_, resourceType_, resourceId_, action_, logData_, logDataSize_);
+      }
+    };
+
+    Operations operations(userId, resourceType, resourceId, action, logData, logDataSize);
+    Apply(operations);
+  }
+
 }
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Fri Jul 11 17:33:28 2025 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Mon Jul 14 16:10:24 2025 +0200
@@ -491,6 +491,17 @@
       {
         return transaction_.SetAttachmentCustomData(attachmentUuid, customData, customDataSize);
       }
+
+      void RecordAuditLog(const std::string& userId,
+                          ResourceType resourceType,
+                          const std::string& resourceId,
+                          const std::string& action,
+                          const void* logData,
+                          size_t logDataSize)
+      {
+        return transaction_.RecordAuditLog(userId, resourceType, resourceId, action, logData, logDataSize);
+      }
+
     };
 
 
@@ -620,7 +631,9 @@
     bool HasKeyValueStoresSupport();
 
     bool HasQueuesSupport();
-    
+
+    bool HasAuditLogsSupport();
+
     void GetExportedResources(Json::Value& target,
                               int64_t since,
                               uint32_t limit);
@@ -879,5 +892,12 @@
 
       const std::string& GetValue() const;
     };
+
+    void RecordAuditLog(const std::string& userId,
+                        ResourceType resourceType,
+                        const std::string& resourceId,
+                        const std::string& action,
+                        const void* logData,
+                        size_t logDataSize);
   };
 }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Fri Jul 11 17:33:28 2025 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Mon Jul 14 16:10:24 2025 +0200
@@ -97,6 +97,7 @@
     static const char* const HAS_EXTENDED_CHANGES = "HasExtendedChanges";
     static const char* const HAS_KEY_VALUE_STORES = "HasKeyValueStores";
     static const char* const HAS_QUEUES = "HasQueues";
+    static const char* const HAS_AUDITS_LOGS = "HasAuditLogs";
     static const char* const HAS_EXTENDED_FIND = "HasExtendedFind";
     static const char* const READ_ONLY = "ReadOnly";
 
@@ -215,6 +216,7 @@
     result[CAPABILITIES][HAS_EXTENDED_FIND] = OrthancRestApi::GetIndex(call).HasFindSupport();
     result[CAPABILITIES][HAS_KEY_VALUE_STORES] = OrthancRestApi::GetIndex(call).HasKeyValueStoresSupport();
     result[CAPABILITIES][HAS_QUEUES] = OrthancRestApi::GetIndex(call).HasQueuesSupport();
+    result[CAPABILITIES][HAS_AUDITS_LOGS] = OrthancRestApi::GetIndex(call).HasAuditLogsSupport();
     
     call.GetOutput().AnswerJson(result);
   }