From cd0609fe9c40d559d4ff78654299e760af45c079 Mon Sep 17 00:00:00 2001
From: Miroslav Tsvetanov
Date: Fri, 24 Jul 2026 16:49:21 +0300
Subject: [PATCH 1/6] test
---
pom.xml | 1 +
1 file changed, 1 insertion(+)
diff --git a/pom.xml b/pom.xml
index 25799d1..f979bba 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2,6 +2,7 @@
xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
+
The Apache Software License, Version 2.0
From 9da78e29d4d003eabfbfd2f2384e3322152c15ff Mon Sep 17 00:00:00 2001
From: Miroslav Tsvetanov
Date: Wed, 12 Aug 2026 09:09:34 -0700
Subject: [PATCH 2/6] Release 0.0.5
---
pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pom.xml b/pom.xml
index b75656a..5259140 100644
--- a/pom.xml
+++ b/pom.xml
@@ -24,7 +24,7 @@
- 0.0.4
+ 0.0.5
17
${java.version}
UTF-8
From 448f14eaef1c720e0931d64ea6b9e99241b87f8c Mon Sep 17 00:00:00 2001
From: Miroslav Tsvetanov
Date: Tue, 25 Aug 2026 19:22:20 +0300
Subject: [PATCH 3/6] init act as version
---
.../auditlog/ng/AuditLogNGHandler.java | 174 +++++++++++--
.../auditlog/ng/AuditLogNGHandlerTest.java | 241 +++++++++++++++++-
2 files changed, 397 insertions(+), 18 deletions(-)
diff --git a/cds-feature-auditlog-ng/src/main/java/com/sap/cds/feature/auditlog/ng/AuditLogNGHandler.java b/cds-feature-auditlog-ng/src/main/java/com/sap/cds/feature/auditlog/ng/AuditLogNGHandler.java
index 86fe4b9..4325219 100644
--- a/cds-feature-auditlog-ng/src/main/java/com/sap/cds/feature/auditlog/ng/AuditLogNGHandler.java
+++ b/cds-feature-auditlog-ng/src/main/java/com/sap/cds/feature/auditlog/ng/AuditLogNGHandler.java
@@ -38,6 +38,7 @@
import java.time.Instant;
import java.util.Collection;
+import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
@@ -71,6 +72,23 @@ public class AuditLogNGHandler implements EventHandler {
private static final String ANONYMOUS_USER = "anonymous";
private static final String NULL_VALUE = "null";
+ /**
+ * Payload key holding the {@code common} section overrides supplied by the client as a nested map, e.g.
+ * {@code data.put("common", Map.of("userInitiatorId", ..., "userInitiatorContext", ...))}.
+ * The value must be a {@code Map} following the Audit Event v2 {@code common} / {@code UserContext} schema.
+ */
+ private static final String COMMON_OVERRIDE_KEY = "common";
+
+ /**
+ * Fields of the Audit Event v2 {@code common} section that can be overridden via the event payload.
+ * Each field is read from the {@link #COMMON_OVERRIDE_KEY} override map.
+ */
+ private static final List COMMON_OVERRIDE_FIELDS = List.of(
+ "userInitiatorId",
+ "userImpersonatedId",
+ "userInitiatorContext",
+ "userImpersonatedContext");
+
private final AuditLogNGCommunicator communicator;
private final TenantProviderService tenantService;
@@ -134,7 +152,7 @@ private ArrayNode createGeneralEvent(EventContext context) throws JsonProcessing
private ObjectNode createGeneralEventNode(String eventType, UserInfo userInfo, ObjectNode eventNode) {
String formattedEventType = formatEventTypeToV2(eventType);
ObjectNode eventEnvelope = buildEventEnvelope(formattedEventType, userInfo, null);
- ObjectNode common = buildEventCommonSection(userInfo);
+ ObjectNode common = buildEventCommonSection(userInfo, null);
ObjectNode dataNode = createGeneralEventDataNode(formattedEventType, common, eventNode);
eventEnvelope.set("data", dataNode);
return eventEnvelope;
@@ -174,7 +192,7 @@ private ArrayNode createSecurityEvent(SecurityLogContext context) {
SecurityLog data = requireNonNull(context.getData(), "SecurityLogContext.getData() is null");
UserInfo userInfo = requireNonNull(context.getUserInfo(), "SecurityLogContext.getUserInfo() is null");
ObjectNode alsEvent = buildLegacySecurityEventEnvelope(LEGACY_SECURITY_WRAPPER, userInfo, data);
- ObjectNode metadata = buildLegacySecurityEventMetadata(userInfo);
+ ObjectNode metadata = buildLegacySecurityEventMetadata(userInfo, data);
ObjectNode origEvent = createLegacySecurityOrigEvent(userInfo, data);
ObjectNode legacySecurityWrapper = OBJECT_MAPPER.createObjectNode();
try {
@@ -201,10 +219,13 @@ private ObjectNode buildLegacySecurityEventEnvelope(String type, UserInfo userIn
return alsEvent;
}
- private ObjectNode buildLegacySecurityEventMetadata(UserInfo userInfo) {
+ private ObjectNode buildLegacySecurityEventMetadata(UserInfo userInfo, CdsData payload) {
ObjectNode metadata = OBJECT_MAPPER.createObjectNode();
metadata.put("ts", Instant.now().toString());
metadata.put("userInitiatorId", userInfo.getName() != null ? userInfo.getName() : ANONYMOUS_USER);
+ // Payload overrides for the native v1 metadata identity fields
+ setFieldIfNotNull(metadata, "userInitiatorId", commonOverride(payload, "userInitiatorId"));
+ setFieldIfNotNull(metadata, "userImpersonatedId", commonOverride(payload, "userImpersonatedId"));
ObjectNode infraOther = metadata.putObject("infrastructure").putObject("other");
infraOther.put("runtimeType", "Java");
ObjectNode platformOther = metadata.putObject("platform").putObject("other");
@@ -224,7 +245,7 @@ private ObjectNode buildAuditLogEventDataV1(ObjectNode metadata, ObjectNode data
*
* The resulting JSON object includes the following fields:
* - uuid: A randomly generated UUID string for the event
- * - user: The name of the user from userInfo, or "unknown" if userInfo is null
+ * - user: The acting user, preferring the {@code userInitiatorId} override, then userInfo, then "unknown"
* - identityProvider: A constant value "$IDP"
* - time: The current timestamp as an ISO-8601 string
* - data: The data from the SecurityLog object, or an empty string if data is null
@@ -238,18 +259,60 @@ private ObjectNode createLegacySecurityOrigEvent(UserInfo userInfo, SecurityLog
String formattedData = "action: %s, data: %s".formatted(data.getAction(), data.getData());
formattedData = formattedData.replace("\r\n", "\\n").replace("\n", "\\n");
setFieldIfNotNull(envelop, "uuid", UUID.randomUUID().toString());
- setFieldIfNotNull(envelop, "user", userInfo.getName() != null ? userInfo.getName() : "unknown");
+ setFieldIfNotNull(envelop, "user", resolveInitiatorUser(userInfo, data));
setFieldIfNotNull(envelop, "identityProvider", "$IDP");
setFieldIfNotNull(envelop, "time", Instant.now().toString());
setFieldIfNotNull(envelop, "data", formattedData);
- if (Boolean.TRUE.equals(userInfo.getAdditionalAttribute(ATTRIBUTE_SAP_SUPPORT_USER))) {
- ObjectNode customDetails = OBJECT_MAPPER.createObjectNode();
- customDetails.put(ATTRIBUTE_SAP_SUPPORT_USER, true);
- envelop.set("customDetails", customDetails);
- }
+ addLegacySecurityCustomDetails(envelop, userInfo, data);
return envelop;
}
+ /**
+ * Resolves the acting user for the legacy {@code origEvent}, applying the same precedence used across the
+ * {@code common} section: a {@code userInitiatorId} supplied in the {@code common} override map wins, with a
+ * fallback to {@link UserInfo#getName()} and finally {@code "unknown"}.
+ *
+ * @param userInfo the user information used as the fallback
+ * @param payload the event payload that may contain a {@code userInitiatorId} override, can be {@code null}
+ * @return the resolved acting user
+ */
+ private String resolveInitiatorUser(UserInfo userInfo, CdsData payload) {
+ Object override = commonOverride(payload, "userInitiatorId");
+ if (override instanceof String initiator && !initiator.isEmpty()) {
+ return initiator;
+ }
+ return userInfo.getName() != null ? userInfo.getName() : "unknown";
+ }
+
+ /**
+ * Adds the {@code customDetails} block of a legacy security {@code origEvent}.
+ *
+ * The v1 metadata schema has no extended user-context fields, so the initiator/impersonated
+ * {@code UserContext} objects are carried here, keeping the v2 {@code UserContext} shape
+ * ({@code type} plus a {@code string}-to-{@code string} {@code attributes} map).
+ *
+ * The SAP support-user signal is represented purely through the initiator context {@code type}
+ * ({@code USER_TYPE_SAP_SUPPORT_USER}): the {@code common} override's {@code userInitiatorContext} is
+ * primary, and {@link UserInfo} is only a rollback used to synthesize that context when no override is
+ * present (see {@link #resolveUserInitiatorContext(UserInfo, CdsData)}). The block is emitted whenever an
+ * effective initiator context or an impersonated context override exists.
+ *
+ * @param envelop the {@code origEvent} node to update in place
+ * @param userInfo the user information used as the support-user rollback
+ * @param payload the event payload that may contain context overrides, can be {@code null}
+ */
+ private void addLegacySecurityCustomDetails(ObjectNode envelop, UserInfo userInfo, CdsData payload) {
+ Object userInitiatorContext = resolveUserInitiatorContext(userInfo, payload);
+ Object userImpersonatedContext = commonOverride(payload, "userImpersonatedContext");
+ if (userInitiatorContext == null && userImpersonatedContext == null) {
+ return;
+ }
+ ObjectNode customDetails = OBJECT_MAPPER.createObjectNode();
+ setFieldIfNotNull(customDetails, "userInitiatorContext", userInitiatorContext);
+ setFieldIfNotNull(customDetails, "userImpersonatedContext", userImpersonatedContext);
+ envelop.set("customDetails", customDetails);
+ }
+
public void handleDataAccessEvent(DataAccessLogContext context) throws JsonProcessingException {
ArrayNode alsEvents = createAlsDataAccessEvents(context);
communicator.sendBulkRequest(alsEvents, false);
@@ -361,7 +424,7 @@ private ArrayNode createAlsConfigChangeEvents(ConfigChangeLogContext context) {
*/
private ObjectNode buildConfigChangeEvent(UserInfo userInfo, ConfigChange configChanges, ChangedAttribute attribute, CdsData payload) {
DataObject dataObject = requireNonNull(configChanges.getDataObject(), "ConfigChange.getDataObject() is null");
- ObjectNode common = buildEventCommonSection(userInfo);
+ ObjectNode common = buildEventCommonSection(userInfo, payload);
ObjectNode dataNode = createConfigurationChangeDataNode(attribute, dataObject, common);
return buildAlsEvent(EVENT_TYPE_CONFIGURATION_CHANGE, userInfo, dataNode, payload);
}
@@ -432,7 +495,7 @@ private ArrayNode buildAttributeBasedAlsEvents(UserInfo userInfo, CollectionValues derived from {@link UserInfo} act as defaults and can be overridden per request via
+ * the event payload, see {@link #applyCommonOverridesFromPayload(ObjectNode, CdsData)}.
+ *
* @param userInfo the user information used to populate the common section
+ * @param payload the event payload that may contain common-section overrides, can be {@code null}
* @return an {@link ObjectNode} representing the common section
*/
- private ObjectNode buildEventCommonSection(UserInfo userInfo) {
+ private ObjectNode buildEventCommonSection(UserInfo userInfo, CdsData payload) {
ObjectNode common = OBJECT_MAPPER.createObjectNode();
String tenant = resolveTenant(userInfo);
common.put("tenantId", tenant);
common.put("userInitiatorId", userInfo.getName() != null ? userInfo.getName() : ANONYMOUS_USER);
+ setFieldIfNotNull(common, "userInitiatorContext", resolveUserInitiatorContext(userInfo, payload));
+ applyCommonOverridesFromPayload(common, payload);
+ return common;
+ }
+
+ /**
+ * Resolves the effective {@code userInitiatorContext} using a single, consistent precedence shared by the
+ * v2 {@code common} section and the legacy security {@code customDetails}.
+ *
+ * The SAP support-user signal is expressed through the context {@code type}
+ * ({@code USER_TYPE_SAP_SUPPORT_USER}): a {@code userInitiatorContext} supplied in the {@code common}
+ * override map is primary, and {@link UserInfo#getAdditionalAttribute(String)} ({@code sap_support_user})
+ * is only a rollback used to synthesize a {@code {type: USER_TYPE_SAP_SUPPORT_USER}} context when no
+ * override is present.
+ *
+ * @param userInfo the user information used as the support-user rollback
+ * @param payload the event payload that may contain a {@code userInitiatorContext} override, can be {@code null}
+ * @return the effective initiator context (override value or a synthesized support-user context), or {@code null}
+ */
+ private Object resolveUserInitiatorContext(UserInfo userInfo, CdsData payload) {
+ Object override = commonOverride(payload, "userInitiatorContext");
+ if (override != null) {
+ return override;
+ }
if (Boolean.TRUE.equals(userInfo.getAdditionalAttribute(ATTRIBUTE_SAP_SUPPORT_USER))) {
ObjectNode userInitiatorContext = OBJECT_MAPPER.createObjectNode();
userInitiatorContext.put("type", USER_TYPE_SAP_SUPPORT_USER);
- common.set("userInitiatorContext", userInitiatorContext);
+ return userInitiatorContext;
}
- return common;
+ return null;
+ }
+
+ /**
+ * Overrides fields of the {@code common} section with values supplied in the event payload.
+ *
+ * The client provides overrides as a nested map under the {@link #COMMON_OVERRIDE_KEY} key, e.g.
+ * {@code data.put("common", Map.of("userInitiatorId", ..., "userImpersonatedId", ...,
+ * "userInitiatorContext", Map.of("type", ...), "userImpersonatedContext", Map.of("type", ...)))}.
+ * Only the allow-listed fields (see {@link #COMMON_OVERRIDE_FIELDS}) are applied. Override values must
+ * follow the v2 schema shape: strings for identifiers and {@code UserContext} objects (with {@code type}
+ * and optional {@code attributes}) for the context fields. Absent or {@code null} entries leave the
+ * computed defaults untouched.
+ *
+ * @param common the common section to update in place
+ * @param payload the event payload that may contain override entries, can be {@code null}
+ */
+ private void applyCommonOverridesFromPayload(ObjectNode common, CdsData payload) {
+ Map overrides = commonOverrides(payload);
+ if (overrides == null) {
+ return;
+ }
+ for (String field : COMMON_OVERRIDE_FIELDS) {
+ setFieldIfNotNull(common, field, overrides.get(field));
+ }
+ }
+
+ /**
+ * Reads a single {@code common} override value from the event payload.
+ *
+ * @param payload the event payload, can be {@code null}
+ * @param field the v2 common field name
+ * @return the override value, or {@code null} if the payload has no {@code common} override map or the entry is absent
+ */
+ private Object commonOverride(CdsData payload, String field) {
+ Map overrides = commonOverrides(payload);
+ return overrides == null ? null : overrides.get(field);
+ }
+
+ /**
+ * Extracts the client-supplied {@code common} override map from the event payload.
+ *
+ * @param payload the event payload, can be {@code null}
+ * @return the override map, or {@code null} when absent or not a {@code Map}
+ */
+ @SuppressWarnings("unchecked")
+ private Map commonOverrides(CdsData payload) {
+ if (payload == null) {
+ return null;
+ }
+ Object common = payload.get(COMMON_OVERRIDE_KEY);
+ return common instanceof Map ? (Map) common : null;
}
/**
@@ -539,7 +681,7 @@ private ObjectNode buildEventCommonSection(UserInfo userInfo) {
* @return an {@link ObjectNode} representing the constructed ALS event for data access
*/
private ObjectNode buildDataAccessAlsEvent(UserInfo userInfo, Access access, String attribute, String attachmentType, String attachmentId, CdsData payload) {
- ObjectNode common = buildEventCommonSection(userInfo);
+ ObjectNode common = buildEventCommonSection(userInfo, payload);
ObjectNode dataNode = createDppDataAccessDataNode(access, attribute, attachmentType, attachmentId, common);
return buildAlsEvent(EVENT_TYPE_DPP_DATA_ACCESS, userInfo, dataNode, payload);
}
diff --git a/cds-feature-auditlog-ng/src/test/java/com/sap/cds/feature/auditlog/ng/AuditLogNGHandlerTest.java b/cds-feature-auditlog-ng/src/test/java/com/sap/cds/feature/auditlog/ng/AuditLogNGHandlerTest.java
index 5f4d689..c6fe777 100644
--- a/cds-feature-auditlog-ng/src/test/java/com/sap/cds/feature/auditlog/ng/AuditLogNGHandlerTest.java
+++ b/cds-feature-auditlog-ng/src/test/java/com/sap/cds/feature/auditlog/ng/AuditLogNGHandlerTest.java
@@ -570,6 +570,17 @@ private ConfigChange mockConfigChange(List attrs, DataObject o
return cc;
}
+ /**
+ * Standard SAP support-user {@code common} override map used across the per-event-type override tests.
+ */
+ private static Map sapSupportCommonOverride() {
+ return Map.of(
+ "userInitiatorId", "P0000support01@sap.com",
+ "userImpersonatedId", "S-User",
+ "userInitiatorContext", Map.of("type", "USER_TYPE_SAP_SUPPORT_USER"),
+ "userImpersonatedContext", Map.of("type", "USER_TYPE_BUSINESS_USER"));
+ }
+
@Test
public void testSapSupportUser_SetsUserInitiatorContext() throws Exception {
when(userInfo.getAdditionalAttribute("sap_support_user")).thenReturn(true);
@@ -595,6 +606,8 @@ public void testSapSupportUser_SetsUserInitiatorContext() throws Exception {
JsonNode common = captor.getValue().get(0).get("data").get("common");
assertTrue(common.has("userInitiatorContext"), "userInitiatorContext should be set for SAP support user");
assertEquals("USER_TYPE_SAP_SUPPORT_USER", common.get("userInitiatorContext").get("type").asText());
+ assertFalse(common.has("userImpersonatedContext"),
+ "a support user in userInfo must not auto-fill userImpersonatedContext (initiator-only fallback)");
}
@Test
@@ -645,8 +658,9 @@ public void testSapSupportUser_SetsAttributeInLegacySecurityEventMetadata() thro
JsonNode origEvent = new com.fasterxml.jackson.databind.ObjectMapper().readTree(origEventJson);
JsonNode customDetails = origEvent.get("customDetails");
assertNotNull(customDetails, "customDetails should be present in origEvent for SAP support user");
- assertTrue(customDetails.has("sap_support_user"), "sap_support_user should be present in customDetails");
- assertTrue(customDetails.get("sap_support_user").asBoolean(), "sap_support_user should be true");
+ assertFalse(customDetails.has("sap_support_user"), "sap_support_user boolean should no longer be emitted");
+ assertEquals("USER_TYPE_SAP_SUPPORT_USER", customDetails.get("userInitiatorContext").get("type").asText(),
+ "SAP support user should be represented as userInitiatorContext.type");
}
@Test
@@ -668,6 +682,229 @@ public void testNonSapSupportUser_DoesNotSetAttributeInLegacySecurityEventMetada
assertFalse(metadata.has("sap_support_user"), "sap_support_user should NOT be present for non-support user");
}
+ @Test
+ public void testDataAccessLog_AppliesCommonOverridesFromPayload() throws Exception {
+ // Given: the client attaches support-user identity via the "common" override map
+ when(userInfo.getName()).thenReturn("regular-user");
+
+ KeyValuePair id = mockKeyValuePair("userId", "user-1");
+ DataObject dataObject = mockDataObject("User", List.of(id));
+ DataSubject dataSubject = mockDataSubject("Person", List.of(id));
+ Attribute attr = mockAttribute("email");
+ Access access = mock(Access.class);
+ when(access.getDataObject()).thenReturn(dataObject);
+ when(access.getDataSubject()).thenReturn(dataSubject);
+ when(access.getAttributes()).thenReturn(List.of(attr));
+ DataAccessLog dataAccessLog = mock(DataAccessLog.class);
+ when(dataAccessLog.getAccesses()).thenReturn(List.of(access));
+
+ Map commonOverride = Map.of(
+ "userInitiatorId", "P0000support01@sap.com",
+ "userImpersonatedId", "S-User",
+ "userInitiatorContext", Map.of(
+ "type", "USER_TYPE_SAP_SUPPORT_USER",
+ "attributes", Map.of("support_ticket", "INC-2026-88231", "idp", "sap.support-idp")),
+ "userImpersonatedContext", Map.of("type", "USER_TYPE_BUSINESS_USER"));
+ when(dataAccessLog.get("common")).thenReturn(commonOverride);
+
+ DataAccessLogContext context = mock(DataAccessLogContext.class);
+ when(context.getData()).thenReturn(dataAccessLog);
+ when(context.getUserInfo()).thenReturn(userInfo);
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(ArrayNode.class);
+ handler.handleDataAccessEvent(context);
+ verify(communicator).sendBulkRequest(captor.capture(), eq(false));
+
+ // Then: the overrides land in the v2 common section (not metadata), preserving the UserContext shape
+ JsonNode common = captor.getValue().get(0).get("data").get("common");
+ assertEquals("P0000support01@sap.com", common.get("userInitiatorId").asText(),
+ "userInitiatorId should come from the common override");
+ assertEquals("S-User", common.get("userImpersonatedId").asText(),
+ "userImpersonatedId should come from the common override");
+ assertEquals("USER_TYPE_SAP_SUPPORT_USER", common.get("userInitiatorContext").get("type").asText());
+ assertEquals("INC-2026-88231", common.get("userInitiatorContext").get("attributes").get("support_ticket").asText(),
+ "UserContext.attributes should be preserved");
+ assertEquals("sap.support-idp", common.get("userInitiatorContext").get("attributes").get("idp").asText());
+ assertEquals("USER_TYPE_BUSINESS_USER", common.get("userImpersonatedContext").get("type").asText());
+ }
+
+ @Test
+ public void testSecurityLog_AppliesCommonOverridesFromPayload() throws Exception {
+ // Given: the client attaches support-user identity via the "common" override map on a legacy security event
+ when(userInfo.getName()).thenReturn("regular-user");
+
+ SecurityLogContext context = mock(SecurityLogContext.class);
+ SecurityLog securityLog = mock(SecurityLog.class);
+ when(context.getUserInfo()).thenReturn(userInfo);
+ when(context.getData()).thenReturn(securityLog);
+ when(securityLog.getData()).thenReturn("security event data");
+
+ Map commonOverride = sapSupportCommonOverride();
+ when(securityLog.get("common")).thenReturn(commonOverride);
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(ArrayNode.class);
+ handler.handleSecurityEvent(context);
+ verify(communicator).sendBulkRequest(captor.capture(), eq(true));
+
+ JsonNode data = captor.getValue().get(0).get("data");
+
+ // Then: the identifiers map to the native v1 metadata fields
+ JsonNode metadata = data.get("metadata");
+ assertEquals("P0000support01@sap.com", metadata.get("userInitiatorId").asText(),
+ "userInitiatorId should override the v1 metadata field");
+ assertEquals("S-User", metadata.get("userImpersonatedId").asText(),
+ "userImpersonatedId should override the v1 metadata field");
+
+ // And: the UserContext objects (unsupported by v1 metadata) are carried in origEvent.customDetails
+ String origEventJson = data.get("data").get("legacySecurityWrapper").get("origEvent").asText();
+ JsonNode origEvent = OBJECT_MAPPER.readTree(origEventJson);
+ assertEquals("P0000support01@sap.com", origEvent.get("user").asText(),
+ "origEvent.user should honor the userInitiatorId override, not userInfo.getName()");
+ JsonNode customDetails = origEvent.get("customDetails");
+ assertNotNull(customDetails, "customDetails should carry the UserContext overrides");
+ assertEquals("USER_TYPE_SAP_SUPPORT_USER", customDetails.get("userInitiatorContext").get("type").asText());
+ assertEquals("USER_TYPE_BUSINESS_USER", customDetails.get("userImpersonatedContext").get("type").asText());
+ assertFalse(metadata.has("userInitiatorContext"), "UserContext must not be placed in v1 metadata");
+ assertFalse(metadata.has("userImpersonatedContext"), "UserContext must not be placed in v1 metadata");
+ }
+
+ @Test
+ public void testConfigChangeLog_AppliesCommonOverridesFromPayload() throws Exception {
+ when(userInfo.getName()).thenReturn("regular-user");
+
+ ChangedAttribute attr = mockChangedAttribute("logLevel", "INFO", "DEBUG");
+ KeyValuePair id = mockKeyValuePair("appId", "app-1");
+ DataObject dataObject = mockDataObject("AppConfig", List.of(id));
+ ConfigChange config = mockConfigChange(List.of(attr), dataObject);
+ ConfigChangeLog configChangeLog = mock(ConfigChangeLog.class);
+ when(configChangeLog.getConfigurations()).thenReturn(List.of(config));
+
+ Map commonOverride = sapSupportCommonOverride();
+ when(configChangeLog.get("common")).thenReturn(commonOverride);
+
+ ConfigChangeLogContext context = mock(ConfigChangeLogContext.class);
+ when(context.getData()).thenReturn(configChangeLog);
+ when(context.getUserInfo()).thenReturn(userInfo);
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(ArrayNode.class);
+ handler.handleConfigChangeEvent(context);
+ verify(communicator).sendBulkRequest(captor.capture(), eq(false));
+
+ JsonNode common = captor.getValue().get(0).get("data").get("common");
+ assertEquals("P0000support01@sap.com", common.get("userInitiatorId").asText());
+ assertEquals("S-User", common.get("userImpersonatedId").asText());
+ assertEquals("USER_TYPE_SAP_SUPPORT_USER", common.get("userInitiatorContext").get("type").asText());
+ assertEquals("USER_TYPE_BUSINESS_USER", common.get("userImpersonatedContext").get("type").asText());
+ }
+
+ @Test
+ public void testDataModificationLog_AppliesCommonOverridesFromPayload() throws Exception {
+ when(userInfo.getName()).thenReturn("regular-user");
+
+ ChangedAttribute attr = mockChangedAttribute("email", "old@example.com", "new@example.com");
+ KeyValuePair id = mockKeyValuePair("userId", "user-1");
+ DataObject dataObject = mockDataObject("User", List.of(id));
+ DataSubject dataSubject = mockDataSubject("Person", List.of(id));
+ DataModification modification = mockDataModification(List.of(attr), dataObject, dataSubject);
+ DataModificationLog dataModificationLog = mock(DataModificationLog.class);
+ when(dataModificationLog.getModifications()).thenReturn(List.of(modification));
+
+ Map commonOverride = sapSupportCommonOverride();
+ when(dataModificationLog.get("common")).thenReturn(commonOverride);
+
+ DataModificationLogContext context = mock(DataModificationLogContext.class);
+ when(context.getData()).thenReturn(dataModificationLog);
+ when(context.getUserInfo()).thenReturn(userInfo);
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(ArrayNode.class);
+ handler.handleDataModificationEvent(context);
+ verify(communicator).sendBulkRequest(captor.capture(), eq(false));
+
+ JsonNode common = captor.getValue().get(0).get("data").get("common");
+ assertEquals("P0000support01@sap.com", common.get("userInitiatorId").asText());
+ assertEquals("S-User", common.get("userImpersonatedId").asText());
+ assertEquals("USER_TYPE_SAP_SUPPORT_USER", common.get("userInitiatorContext").get("type").asText());
+ assertEquals("USER_TYPE_BUSINESS_USER", common.get("userImpersonatedContext").get("type").asText());
+ }
+
+ @Test
+ public void testDataAccessLog_PartialCommonOverride_KeepsDefaults() throws Exception {
+ // Given: only userInitiatorId is overridden; the other fields are absent from the override map
+ when(userInfo.getName()).thenReturn("regular-user");
+
+ KeyValuePair id = mockKeyValuePair("userId", "user-1");
+ DataObject dataObject = mockDataObject("User", List.of(id));
+ DataSubject dataSubject = mockDataSubject("Person", List.of(id));
+ Attribute attr = mockAttribute("email");
+ Access access = mock(Access.class);
+ when(access.getDataObject()).thenReturn(dataObject);
+ when(access.getDataSubject()).thenReturn(dataSubject);
+ when(access.getAttributes()).thenReturn(List.of(attr));
+ DataAccessLog dataAccessLog = mock(DataAccessLog.class);
+ when(dataAccessLog.getAccesses()).thenReturn(List.of(access));
+
+ Map commonOverride = Map.of("userInitiatorId", "P0000support01@sap.com");
+ when(dataAccessLog.get("common")).thenReturn(commonOverride);
+
+ DataAccessLogContext context = mock(DataAccessLogContext.class);
+ when(context.getData()).thenReturn(dataAccessLog);
+ when(context.getUserInfo()).thenReturn(userInfo);
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(ArrayNode.class);
+ handler.handleDataAccessEvent(context);
+ verify(communicator).sendBulkRequest(captor.capture(), eq(false));
+
+ // Then: the provided field is overridden, and absent fields are not injected
+ JsonNode common = captor.getValue().get(0).get("data").get("common");
+ assertEquals("P0000support01@sap.com", common.get("userInitiatorId").asText(),
+ "userInitiatorId should be overridden");
+ assertFalse(common.has("userImpersonatedId"), "userImpersonatedId should not be set when absent from override");
+ assertFalse(common.has("userInitiatorContext"), "userInitiatorContext should not be set when absent from override");
+ assertFalse(common.has("userImpersonatedContext"), "userImpersonatedContext should not be set when absent from override");
+ }
+
+ @Test
+ public void testDataAccessLog_CommonOverride_TakesPrecedence_OverUserInfo() throws Exception {
+ // Given: userInfo would produce its own defaults (name + SAP support user context)...
+ when(userInfo.getName()).thenReturn("userinfo-fallback-user");
+ when(userInfo.getAdditionalAttribute("sap_support_user")).thenReturn(true);
+
+ KeyValuePair id = mockKeyValuePair("userId", "user-1");
+ DataObject dataObject = mockDataObject("User", List.of(id));
+ DataSubject dataSubject = mockDataSubject("Person", List.of(id));
+ Attribute attr = mockAttribute("email");
+ Access access = mock(Access.class);
+ when(access.getDataObject()).thenReturn(dataObject);
+ when(access.getDataSubject()).thenReturn(dataSubject);
+ when(access.getAttributes()).thenReturn(List.of(attr));
+ DataAccessLog dataAccessLog = mock(DataAccessLog.class);
+ when(dataAccessLog.getAccesses()).thenReturn(List.of(access));
+
+ // ...but the override supplies different values, which must win
+ Map commonOverride = Map.of(
+ "userInitiatorId", "override-initiator",
+ "userInitiatorContext", Map.of(
+ "type", "USER_TYPE_BUSINESS_USER",
+ "attributes", Map.of("origin", "override")));
+ when(dataAccessLog.get("common")).thenReturn(commonOverride);
+
+ DataAccessLogContext context = mock(DataAccessLogContext.class);
+ when(context.getData()).thenReturn(dataAccessLog);
+ when(context.getUserInfo()).thenReturn(userInfo);
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(ArrayNode.class);
+ handler.handleDataAccessEvent(context);
+ verify(communicator).sendBulkRequest(captor.capture(), eq(false));
+
+ // Then: override values win over the userInfo-derived defaults
+ JsonNode common = captor.getValue().get(0).get("data").get("common");
+ assertEquals("override-initiator", common.get("userInitiatorId").asText(),
+ "override userInitiatorId must take precedence over userInfo.getName()");
+ assertEquals("USER_TYPE_BUSINESS_USER", common.get("userInitiatorContext").get("type").asText(),
+ "override userInitiatorContext must take precedence over the SAP support user default");
+ assertEquals("override", common.get("userInitiatorContext").get("attributes").get("origin").asText());
+ }
+
@Test
public void testResolveTenant_FallsBackToProviderTenant_WhenUserTenantNull() throws Exception {
when(userInfo.getTenant()).thenReturn(null);
From 9636d6bda2a90a410ffdf3650c51a1b5622afb74 Mon Sep 17 00:00:00 2001
From: Miroslav Tsvetanov
Date: Thu, 3 Sep 2026 14:18:43 +0300
Subject: [PATCH 4/6] update schema version
---
.../java/com/sap/cds/feature/auditlog/ng/AuditLogNGHandler.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/cds-feature-auditlog-ng/src/main/java/com/sap/cds/feature/auditlog/ng/AuditLogNGHandler.java b/cds-feature-auditlog-ng/src/main/java/com/sap/cds/feature/auditlog/ng/AuditLogNGHandler.java
index 4325219..99a6ae1 100644
--- a/cds-feature-auditlog-ng/src/main/java/com/sap/cds/feature/auditlog/ng/AuditLogNGHandler.java
+++ b/cds-feature-auditlog-ng/src/main/java/com/sap/cds/feature/auditlog/ng/AuditLogNGHandler.java
@@ -61,7 +61,7 @@ public class AuditLogNGHandler implements EventHandler {
private static final Logger LOGGER = getLogger(AuditLogNGHandler.class);
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final String LEGACY_SECURITY_WRAPPER = "legacySecurityWrapper";
- private static final String DATA_SCHEMA_VERSION = "audit-logging-v2.1.6";
+ private static final String DATA_SCHEMA_VERSION = "v2.2.0";
private static final String DATA_SCHEMA = String.format("https://github.tools.sap/wg-observability/telemetry-semantic-conventions/blob/%s/payload-specifications/audit-logging/proto/sap/auditlog/auditevent/v2/auditevent.proto#/", DATA_SCHEMA_VERSION);
private static final String ATTRIBUTE_SAP_SUPPORT_USER = "sap_support_user";
private static final String USER_TYPE_SAP_SUPPORT_USER = "USER_TYPE_SAP_SUPPORT_USER";
From f0ee839e7fb71b27bafbead9a3af6ef59d6b39c8 Mon Sep 17 00:00:00 2001
From: Miroslav Tsvetanov
Date: Thu, 3 Sep 2026 14:23:45 +0300
Subject: [PATCH 5/6] update version
---
pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pom.xml b/pom.xml
index 7f55bb3..8e9b28d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -24,7 +24,7 @@
- 0.0.5
+ 0.1.0
17
${java.version}
UTF-8
From 919e378e03ffb32c701b894d3802b7305125611d Mon Sep 17 00:00:00 2001
From: simeongelovski <166386217+simeongelovski@users.noreply.github.com>
Date: Thu, 3 Sep 2026 14:58:58 +0300
Subject: [PATCH 6/6] Update AuditLogNGHandler.java
---
.../java/com/sap/cds/feature/auditlog/ng/AuditLogNGHandler.java | 1 -
1 file changed, 1 deletion(-)
diff --git a/cds-feature-auditlog-ng/src/main/java/com/sap/cds/feature/auditlog/ng/AuditLogNGHandler.java b/cds-feature-auditlog-ng/src/main/java/com/sap/cds/feature/auditlog/ng/AuditLogNGHandler.java
index 99a6ae1..b3a8f21 100644
--- a/cds-feature-auditlog-ng/src/main/java/com/sap/cds/feature/auditlog/ng/AuditLogNGHandler.java
+++ b/cds-feature-auditlog-ng/src/main/java/com/sap/cds/feature/auditlog/ng/AuditLogNGHandler.java
@@ -35,7 +35,6 @@
import com.sap.cds.services.utils.CdsErrorStatuses;
import com.sap.cds.services.utils.ErrorStatusException;
import org.slf4j.Logger;
-
import java.time.Instant;
import java.util.Collection;
import java.util.List;