com.sap.cds.feature.auditlog.v2.AuditLogV2Handler Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of cds-feature-auditlog-v2 Show documentation
Show all versions of cds-feature-auditlog-v2 Show documentation
Handler to send auditlog messages to AuditLog Service V2
/**************************************************************************
* (C) 2019-2024 SAP SE or an SAP affiliate company. All rights reserved. *
**************************************************************************/
package com.sap.cds.feature.auditlog.v2;
import java.time.Instant;
import java.util.Collection;
import java.util.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.annotations.VisibleForTesting;
import com.sap.cds.CdsData;
import com.sap.cds.services.EventContext;
import com.sap.cds.services.auditlog.Access;
import com.sap.cds.services.auditlog.Attachment;
import com.sap.cds.services.auditlog.Attribute;
import com.sap.cds.services.auditlog.AuditLogService;
import com.sap.cds.services.auditlog.ChangedAttribute;
import com.sap.cds.services.auditlog.ConfigChange;
import com.sap.cds.services.auditlog.ConfigChangeLogContext;
import com.sap.cds.services.auditlog.DataAccessLogContext;
import com.sap.cds.services.auditlog.DataModification;
import com.sap.cds.services.auditlog.DataModificationLogContext;
import com.sap.cds.services.auditlog.DataObject;
import com.sap.cds.services.auditlog.DataSubject;
import com.sap.cds.services.auditlog.KeyValuePair;
import com.sap.cds.services.auditlog.SecurityLog;
import com.sap.cds.services.auditlog.SecurityLogContext;
import com.sap.cds.services.auditlog.event.TenantOffboardedEventContext;
import com.sap.cds.services.auditlog.event.TenantOnboardedEventContext;
import com.sap.cds.services.auditlog.event.UnauthorizedRequestEventContext;
import com.sap.cds.services.handler.EventHandler;
import com.sap.cds.services.handler.annotations.On;
import com.sap.cds.services.handler.annotations.ServiceName;
import com.sap.cds.services.mt.TenantProviderService;
import com.sap.cds.services.request.UserInfo;
import com.sap.cds.services.utils.CdsErrorStatuses;
import com.sap.cds.services.utils.ErrorStatusException;
import com.sap.cds.services.utils.StringUtils;
import com.sap.cds.repackaged.audit.api.AuditLogMessage;
import com.sap.cds.repackaged.audit.api.exception.AuditLogNotAvailableException;
import com.sap.cds.repackaged.audit.api.exception.AuditLogWriteException;
import com.sap.cds.repackaged.audit.api.v2.AuditLogMessageFactory;
import com.sap.cds.repackaged.audit.api.v2.AuditedDataSubject;
import com.sap.cds.repackaged.audit.api.v2.AuditedObject;
import com.sap.cds.repackaged.audit.api.v2.ConfigurationChangeAuditMessage;
import com.sap.cds.repackaged.audit.api.v2.DataAccessAuditMessage;
import com.sap.cds.repackaged.audit.api.v2.DataModificationAuditMessage;
import com.sap.cds.repackaged.audit.api.v2.SecurityEventAuditMessage;
import com.sap.cds.repackaged.audit.client.impl.Utils;
/**
* Handler that reacts on audit log events to log audit messages with the auditlog V2 API.
*/
@ServiceName(value = "*", type = AuditLogService.class)
public class AuditLogV2Handler implements EventHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(AuditLogV2Handler.class);
private static final String ACTION_DETAILS = "action";
private final AuditLogMessageFactory factory;
private final boolean usesOAuth2;
private final TenantProviderService tenantService;
private static final String SPECIAL_ATTRIBUTE_LOGON_NAME = "logonName";
private final String clientId;
AuditLogV2Handler(AuditLogMessageFactory factory, boolean usesOAuth2, TenantProviderService tenantService, String clientId) {
this.factory = Objects.requireNonNull(factory, "factory must not be null");
this.usesOAuth2 = usesOAuth2;
this.tenantService = tenantService;
this.clientId = clientId;
}
@On
public void handleDataAccessEvent(DataAccessLogContext context) {
Collection dataAccesses = context.getData().getAccesses();
if (dataAccesses != null) {
for (Access dataAccess : dataAccesses) {
DataAccessAuditMessage message = factory.createDataAccessAuditMessage();
message.setDataSubject(getAuditedDataSubject(factory, dataAccess.getDataSubject()));
message.setObject(getAuditedObject(factory, dataAccess.getDataObject()));
Collection attachments = dataAccess.getAttachments();
if (attachments != null) {
attachments.forEach(attachment -> message.addAttachment(attachment.getId(), attachment.getName()));
}
Collection attributes = dataAccess.getAttributes();
if (attributes != null) {
attributes.forEach(attr -> message.addAttribute(attr.getName()));
}
logMessage(message, context.getCreatedAt(), context);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Logged data access with DataObject '{}' and DataSubject '{}'",
dataAccess.getDataObject().toJson(), dataAccess.getDataSubject().toJson());
}
}
}
}
@On
public void handleDataModificationEvent(DataModificationLogContext context) {
Collection modifications = context.getData().getModifications();
if (modifications != null) {
for (DataModification modification : modifications) {
DataModificationAuditMessage message = factory.createDataModificationAuditMessage();
message.setDataSubject(getAuditedDataSubject(factory, modification.getDataSubject()));
message.setObject(getAuditedObject(factory, modification.getDataObject()));
Collection changedAttributes = modification.getAttributes();
if (changedAttributes != null) {
changedAttributes.forEach(change -> message.addAttribute(change.getName(), change.getOldValue(),
change.getNewValue()));
message.addCustomDetails(ACTION_DETAILS, modification.getAction().toString());
}
logMessage(message, context.getCreatedAt(), context);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Logged data modification with DataObject '{}' and DataSubject '{}'",
modification.getDataObject().toJson(), modification.getDataSubject().toJson());
}
}
}
}
@On
public void handleConfigChangeEvent(ConfigChangeLogContext context) {
Collection configurations = context.getData().getConfigurations();
if (configurations != null) {
for (ConfigChange config : configurations) {
ConfigurationChangeAuditMessage message = factory.createConfigurationChangeAuditMessage();
message.setObject(getAuditedObject(factory, config.getDataObject()));
Collection attributes = config.getAttributes();
if (attributes != null) {
attributes.forEach(attribute -> message.addValue(attribute.getName(), attribute.getOldValue(),
attribute.getNewValue()));
message.addCustomDetails(ACTION_DETAILS, context.getData().getAction().toString());
}
logMessage(message, context.getCreatedAt(), context);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Logged config change with DataObject '{}'",
config.getDataObject().toJson());
}
}
}
}
@On
public void handleSecurityEvent(SecurityLogContext context) {
SecurityEventAuditMessage message = factory.createSecurityEventAuditMessage();
SecurityLog data = context.getData();
message.setData("action: %s, data: %s".formatted(data.getAction(), data.getData()));
// TODO: do we need to set?
message.setIp(null);
logMessage(message, context.getCreatedAt(), context);
LOGGER.debug("Logged security event with action '{}'", data.getAction());
}
@On
public void handleTenantOnboardedEvent(TenantOnboardedEventContext context) {
Object event = context.get("data");
if (event != null) {
CdsData eventData = (CdsData)event;
ConfigurationChangeAuditMessage message = factory.createConfigurationChangeAuditMessage();
addCustomDetails(message, eventData);
logMessage(message, (Instant) context.get("createdAt"), context);
LOGGER.debug("Logged tenant onboarded event '{}'", event);
}
}
@On
public void handleTenantOffboardedEvent(TenantOffboardedEventContext context) {
Object event = context.get("data");
if (event != null) {
CdsData eventData = (CdsData)event;
ConfigurationChangeAuditMessage message = factory.createConfigurationChangeAuditMessage();
addCustomDetails(message, eventData);
logMessage(message, (Instant) context.get("createdAt"), context);
LOGGER.debug("Logged tenant offboarded event '{}'", event);
}
}
@On
public void handleUnauthorizedEvent(UnauthorizedRequestEventContext context) {
Object event = context.get("data");
if (event != null) {
CdsData eventData = (CdsData)event;
SecurityEventAuditMessage message = factory.createSecurityEventAuditMessage();
message.setData(((CdsData)eventData.get("data")).toJson());
addCustomDetails(message, eventData);
logMessage(message, (Instant) context.get("createdAt"), context);
LOGGER.debug("Logged unauthorized event '{}'", event);
}
}
private void addCustomDetails(AuditLogMessage message, CdsData event) {
event.entrySet().forEach(e -> message.addCustomDetails(e.getKey(), e.getValue()));
}
@VisibleForTesting
boolean usesOAuth2() {
return this.usesOAuth2;
}
private void logMessage(AuditLogMessage message, Instant createdAt, EventContext context) {
message.setEventTime(createdAt);
UserInfo userInfo = context.getUserInfo();
String tenant = userInfo.getTenant();
String user = userInfo.getName();
if (this.usesOAuth2) {
// OAuth2 plan
// setting user
if (StringUtils.isEmpty(tenant) || userInfo.isSystemUser() || !userInfo.isAuthenticated()) {
// provider tenant or technical user (with subscriber or provider tenant), or
// provider tenant might not be empty (no normalization), but system user, or
// public requests with anonymous user
message.setUser(clientId);
} else {
boolean useLogonName = context.getCdsRuntime().getEnvironment().getCdsProperties().getAuditLog().getV2().isUseLogonName();
String logonName = (String) userInfo.getAdditionalAttribute(SPECIAL_ATTRIBUTE_LOGON_NAME);
if (useLogonName && !StringUtils.isEmpty(logonName)) {
message.setUser(logonName);
} else if (!StringUtils.isEmpty(user)){
message.setUser(user);
} else {
throw new ErrorStatusException(CdsErrorStatuses.AUDITLOG_SERVICE_NO_USER);
}
}
// setting tenant
if (StringUtils.isEmpty(tenant)) {
// if tenant is null -> use provider tenant
LOGGER.debug("User tenant is not set, using the provider tenant.");
message.setTenant(Utils.PROVIDER_VALUE);
} else {
LOGGER.debug("User tenant is set, using the subscriber tenant '{}'.", tenant);
message.setTenant(Utils.SUBSCRIBER_VALUE);
}
// setting idp
message.setIdentityProvider(Utils.IDP_VALUE);
} else {
// standard plan
if (StringUtils.isEmpty(user)) {
throw new ErrorStatusException(CdsErrorStatuses.AUDITLOG_SERVICE_NO_USER);
}
if (StringUtils.isEmpty(tenant)) {
tenant = tenantService.readProviderTenant();
}
LOGGER.debug("Using user '{}' and tenant '{}' to call AuditLog v2 server.", user, tenant);
message.setUser(user);
message.setTenant(tenant);
}
try {
message.log();
} catch (AuditLogNotAvailableException e) {
throw new ErrorStatusException(CdsErrorStatuses.AUDITLOG_SERVICE_NOT_AVAILABLE, e.getMessage(), e);
} catch (AuditLogWriteException e) {
throw new ErrorStatusException(CdsErrorStatuses.AUDITLOG_SERVICE_INVALID_MESSAGE,
String.join(", ", e.getErrors().values()), e);
}
}
// static helpers
private static AuditedDataSubject getAuditedDataSubject(AuditLogMessageFactory factory, DataSubject dataSubject) {
AuditedDataSubject auditedSubject = factory.createAuditedDataSubject();
for (KeyValuePair pair : dataSubject.getId()) {
auditedSubject.addIdentifier(pair.getKeyName(), pair.getValue());
}
auditedSubject.setType(dataSubject.getType());
auditedSubject.setRole(dataSubject.getRole());
return auditedSubject;
}
private static AuditedObject getAuditedObject(AuditLogMessageFactory factory, DataObject dataObject) {
AuditedObject auditedObject = factory.createAuditedObject();
for (KeyValuePair pair : dataObject.getId()) {
auditedObject.addIdentifier(pair.getKeyName(), pair.getValue());
}
auditedObject.setType(dataObject.getType());
return auditedObject;
}
}