All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sap.cds.feature.auditlog.v2.AuditLogV2Handler Maven / Gradle / Ivy

There is a newer version: 3.2.0
Show newest version
/**************************************************************************
 * (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;
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy