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;