feat(server): add SourceNode to the write audit notification payload

The AUDIT_UPDATE_WRITE application notification carries the AttributeId,
IndexRange, NewValue and OldValue of a write, but not the NodeId of the written
node. That NodeId is only set as the SourceNode of the generated
AuditWriteUpdateEvent, so an auditNotificationCallback cannot tell which node
was written without additionally subscribing to the events.

Add /SourceNode (the BaseEventType field name) to the payload. The generated
events are unchanged: the event-field lookup already resolves /SourceNode to
the same NodeId by default, and the RBAC check uses the EventDescription.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ChiouYouJia
2026-09-26 23:49:43 +08:00
committed by Julius Pfrommer
parent e23ad2f4ee
commit 5ae7657e58
2 changed files with 61 additions and 6 deletions
+14 -3
View File
@@ -607,7 +607,7 @@ auditWriteUpdateEvent(UA_Server *server, UA_SecureChannel *channel, UA_Session *
UA_Boolean status, const UA_NodeId *sourceNode,
UA_UInt32 attributeId, const UA_String indexRange,
const UA_Variant *newValue, const UA_Variant *oldValue) {
UA_STATIC_THREAD_LOCAL UA_KeyValuePair writeUpdatePayload[10] = {
UA_STATIC_THREAD_LOCAL UA_KeyValuePair writeUpdatePayload[11] = {
{{0, UA_STRING_STATIC("/ActionTimeStamp")}, {0}}, /* 0 */
{{0, UA_STRING_STATIC("/Status")}, {0}}, /* 1 */
{{0, UA_STRING_STATIC("/ServerId")}, {0}}, /* 2 */
@@ -617,13 +617,24 @@ auditWriteUpdateEvent(UA_Server *server, UA_SecureChannel *channel, UA_Session *
{{0, UA_STRING_STATIC("/AttributeId")}, {0}}, /* 6 */
{{0, UA_STRING_STATIC("/IndexRange")}, {0}}, /* 7 */
{{0, UA_STRING_STATIC("/NewValue")}, {0}}, /* 8 */
{{0, UA_STRING_STATIC("/OldValue")}, {0}} /* 9 */
{{0, UA_STRING_STATIC("/OldValue")}, {0}}, /* 9 */
{{0, UA_STRING_STATIC("/SourceNode")}, {0}} /* 10 */
};
/* /AttributeId */
UA_Variant_setScalar(&writeUpdatePayload[6].value, &attributeId,
&UA_TYPES[UA_TYPES_UINT32]);
/* /SourceNode (the written node). Also exposed in the notification payload
* so that applications can attribute the write without an EventFilter.
* The map is static, so reset the entry if there is no SourceNode. */
if(sourceNode)
UA_Variant_setScalar(&writeUpdatePayload[10].value,
(void*)(uintptr_t)sourceNode,
&UA_TYPES[UA_TYPES_NODEID]);
else
UA_Variant_init(&writeUpdatePayload[10].value);
/* /IndexRange */
UA_Variant_setScalar(&writeUpdatePayload[7].value,
(void*)(uintptr_t)&indexRange,
@@ -639,7 +650,7 @@ auditWriteUpdateEvent(UA_Server *server, UA_SecureChannel *channel, UA_Session *
(UA_Variant*)(uintptr_t)oldValue,
&UA_TYPES[UA_TYPES_VARIANT]);
UA_KeyValueMap payload = {10, writeUpdatePayload};
UA_KeyValueMap payload = {11, writeUpdatePayload};
auditUpdateEvent(server, UA_APPLICATIONNOTIFICATIONTYPE_AUDIT_UPDATE_WRITE,
channel, session, sourceNode, "Write", status, payload);
}
+47 -3
View File
@@ -42,6 +42,11 @@ static UA_atomic(size_t) sessionActivateCalls = 0;
static UA_atomic(size_t) sessionCancelCalls = 0;
static UA_atomic(size_t) channelOpenCalls = 0;
/* Write audits whose payload carries /SourceNode == expectedSourceNode.
* expectedSourceNode is set by the test thread before the write. */
static UA_NodeId expectedSourceNode;
static UA_atomic(size_t) writeSourceNodeMatches = 0;
/* Atomic increment built on UA_atomic_cmpxchg (config.h has no fetch-add) */
static void
counterInc(UA_atomic(size_t) *c) {
@@ -57,11 +62,18 @@ counterInc(UA_atomic(size_t) *c) {
static void
auditCb(UA_Server *s, UA_ApplicationNotificationType type,
const UA_KeyValueMap payload) {
(void)s; (void)payload;
(void)s;
counterInc(&totalAuditCalls);
switch(type) {
case UA_APPLICATIONNOTIFICATIONTYPE_AUDIT_UPDATE_WRITE:
counterInc(&writeAuditCalls); break;
case UA_APPLICATIONNOTIFICATIONTYPE_AUDIT_UPDATE_WRITE: {
counterInc(&writeAuditCalls);
const UA_NodeId *src = (const UA_NodeId*)
UA_KeyValueMap_getScalar(&payload, UA_QUALIFIEDNAME(0, "/SourceNode"),
&UA_TYPES[UA_TYPES_NODEID]);
if(src && UA_NodeId_equal(src, &expectedSourceNode))
counterInc(&writeSourceNodeMatches);
break;
}
case UA_APPLICATIONNOTIFICATIONTYPE_AUDIT_UPDATE_METHOD:
counterInc(&methodAuditCalls); break;
case UA_APPLICATIONNOTIFICATIONTYPE_AUDIT_SECURITY_SESSION_CREATE:
@@ -99,6 +111,8 @@ static void resetCounters(void) {
UA_atomic_store(&sessionActivateCalls, 0);
UA_atomic_store(&sessionCancelCalls, 0);
UA_atomic_store(&channelOpenCalls, 0);
UA_atomic_store(&writeSourceNodeMatches, 0);
expectedSourceNode = UA_NODEID_NULL;
}
static void setup(void) {
@@ -158,6 +172,35 @@ START_TEST(WriteEmitsAuditEvent) {
UA_atomic_load(&totalGlobalCalls) > globalBefore);
} END_TEST
/* Test: the AUDIT_UPDATE_WRITE notification payload carries /SourceNode, the
* NodeId of the written node (the SourceNode of the AuditWriteUpdateEvent). */
START_TEST(WriteAuditPayloadHasSourceNode) {
UA_VariableAttributes attr = UA_VariableAttributes_default;
UA_Int32 v = 0;
UA_Variant_setScalar(&attr.value, &v, &UA_TYPES[UA_TYPES_INT32]);
attr.dataType = UA_TYPES[UA_TYPES_INT32].typeId;
attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE;
UA_NodeId id = UA_NODEID_STRING(1, "audit.var.src");
UA_StatusCode r = UA_Server_addVariableNode(server, id,
UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
UA_QUALIFIEDNAME(1, "audit.var.src"),
UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE),
attr, NULL, NULL);
ck_assert_int_eq(r, UA_STATUSCODE_GOOD);
expectedSourceNode = id;
size_t matchesBefore = UA_atomic_load(&writeSourceNodeMatches);
UA_Variant newVal;
UA_Int32 nv = 5;
UA_Variant_setScalar(&newVal, &nv, &UA_TYPES[UA_TYPES_INT32]);
r = UA_Server_writeValue(server, id, newVal);
ck_assert_int_eq(r, UA_STATUSCODE_GOOD);
ck_assert_uint_gt(UA_atomic_load(&writeSourceNodeMatches), matchesBefore);
} END_TEST
/* Test: with auditingEnabled=false, no audit event is emitted. */
START_TEST(NoAuditWhenDisabled) {
UA_ServerConfig *cfg = UA_Server_getConfig(server);
@@ -273,6 +316,7 @@ static Suite* testSuite(void) {
tcase_add_checked_fixture(tc, setup, teardown);
tcase_set_timeout(tc, 60);
tcase_add_test(tc, WriteEmitsAuditEvent);
tcase_add_test(tc, WriteAuditPayloadHasSourceNode);
tcase_add_test(tc, NoAuditWhenDisabled);
tcase_add_test(tc, ClientConnectEmitsSessionAuditEvents);
tcase_add_test(tc, ToggleWriteUpdateFlag);