mirror of
https://github.com/open62541/open62541.git
synced 2026-10-17 04:42:09 +08:00
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:
committed by
Julius Pfrommer
parent
e23ad2f4ee
commit
5ae7657e58
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user