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

com.spt.development.audit.spring.aop.Auditor Maven / Gradle / Ivy

package com.spt.development.audit.spring.aop;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.spt.development.audit.spring.AuditEvent;
import com.spt.development.audit.spring.AuditEventWriter;
import com.spt.development.audit.spring.Audited;
import com.spt.development.audit.spring.CorrelationIdProvider;
import com.spt.development.audit.spring.DefaultCorrelationIdProvider;
import com.spt.development.audit.spring.security.AuthenticationAdapter;
import com.spt.development.audit.spring.security.AuthenticationAdapterFactory;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.util.ReflectionUtils;

import java.lang.reflect.Field;
import java.lang.reflect.Parameter;
import java.net.UnknownHostException;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.function.BiConsumer;

import static com.spt.development.audit.spring.util.CorrelationIdUtils.addCorrelationIdToArguments;
import static com.spt.development.audit.spring.util.HttpRequestUtils.getClientIpAddress;

/**
 * Aspect that implements auditing for methods annotated with the {@link Audited} annotation.
 */
@Slf4j
@Aspect
public class Auditor {
    private static final Gson GSON = new GsonBuilder().create();

    private static final String DEFAULT_DETAILS_KEY = "DETAILS";

    private final String appName;
    private final String appVersion;
    private final LocalhostFacade localhostFacade;
    private final AuditEventWriter auditEventWriter;
    private final boolean includeCorrelationIdInLogs;
    private final CorrelationIdProvider correlationIdProvider;
    private final AuthenticationAdapterFactory authenticationAdapterFactory;

    /**
     * Creates a new instance of the aspect.
     *
     * @param appName the name of the application that the auditing is for.
     * @param appVersion the version of the application that the auditing is for.
     * @param auditEventWriter the audit event writer that writes the audit logs.
     * @param authenticationAdapterFactory a factory for creating an adapter that is used to retrieve details about the
     *                                     currently authenticated user.
     */
    public Auditor(
            final String appName,
            final String appVersion,
            final AuditEventWriter auditEventWriter,
            final AuthenticationAdapterFactory authenticationAdapterFactory) {
        this(appName, appVersion, auditEventWriter, true, authenticationAdapterFactory);
    }

    /**
     * Creates a new instance of the aspect.
     *
     * @param appName the name of the application that the auditing is for.
     * @param appVersion the version of the application that the auditing is for.
     * @param auditEventWriter the audit event writer that writes the audit logs.
     * @param includeCorrelationIdInLogs a flag to determine whether the correlation ID should be explicitly included
     *                                   in the log statements output by the aspect.
     * @param authenticationAdapterFactory a factory for creating an adapter that is used to retrieve details about the
     *                                     currently authenticated user.
     */
    public Auditor(
            final String appName,
            final String appVersion,
            final AuditEventWriter auditEventWriter,
            final boolean includeCorrelationIdInLogs,
            final AuthenticationAdapterFactory authenticationAdapterFactory) {
        this(appName, appVersion, auditEventWriter, includeCorrelationIdInLogs, new DefaultCorrelationIdProvider(), authenticationAdapterFactory);
    }

    /**
     * Creates a new instance of the aspect.
     *
     * @param appName the name of the application that the auditing is for.
     * @param appVersion the version of the application that the auditing is for.
     * @param auditEventWriter the audit event writer that writes the audit logs.
     * @param includeCorrelationIdInLogs a flag to determine whether the correlation ID should be explicitly included
     *                                   in the log statements output by the aspect.
     * @param correlationIdProvider provider for getting the current correlationId.
     * @param authenticationAdapterFactory a factory for creating an adapter that is used to retrieve details about the
     *                                     currently authenticated user.
     */
    public Auditor(
            final String appName,
            final String appVersion,
            final AuditEventWriter auditEventWriter,
            final boolean includeCorrelationIdInLogs,
            final CorrelationIdProvider correlationIdProvider,
            final AuthenticationAdapterFactory authenticationAdapterFactory) {
        this(appName, appVersion, new LocalhostFacade(), auditEventWriter, includeCorrelationIdInLogs,
                correlationIdProvider, authenticationAdapterFactory);
    }

    /**
     * Creates a new instance of the aspect.
     *
     * @param appName the name of the application that the auditing is for.
     * @param appVersion the version of the application that the auditing is for.
     * @param localhostFacade a facade used to retrieve the hostname of the machine the application is running on.
     * @param auditEventWriter the audit event writer that writes the audit logs.
     * @param includeCorrelationIdInLogs a flag to determine whether the correlation ID should be explicitly included
     *                                   in the log statements output by the aspect.
     * @param correlationIdProvider provider for getting the current correlationId.
     * @param authenticationAdapterFactory a factory for creating an adapter that is used to retrieve details about the
     *                                     currently authenticated user.
     */
    Auditor(
            final String appName,
            final String appVersion,
            final LocalhostFacade localhostFacade,
            final AuditEventWriter auditEventWriter,
            final boolean includeCorrelationIdInLogs,
            final CorrelationIdProvider correlationIdProvider,
            final AuthenticationAdapterFactory authenticationAdapterFactory) {
        this.appName = appName;
        this.appVersion = appVersion;
        this.localhostFacade = localhostFacade;
        this.auditEventWriter = auditEventWriter;
        this.includeCorrelationIdInLogs = includeCorrelationIdInLogs;
        this.correlationIdProvider = correlationIdProvider;
        this.authenticationAdapterFactory = authenticationAdapterFactory;
    }

    /**
     * Generates audit logs for methods annotated with the {@link Audited annotation}. It is envisaged that this will
     * predominantly be methods belonging to classes annotated with the {@link org.springframework.stereotype.Service}
     * annotation, however there is nothing in the implementation that prevents other methods being annotated with the
     * {@link Audited} annotation.
     *
     * 

* For details of usage, see the * README. *

* * @param point the aspect join point required for implementing a {@link Around} aspect. * * @return the value returned from the audited method. * * @throws Exception thrown if the audited method throws a {@link Throwable}. */ @Around("@annotation(com.spt.development.audit.spring.Audited)") public Object audit(ProceedingJoinPoint point) throws Throwable { final Object result = point.proceed(); final MethodSignature signature = (MethodSignature) point.getSignature(); final Audited audited = AnnotatedElementUtils.getMergedAnnotation(signature.getMethod(), Audited.class); if (Audited.NONE.equals(Optional.of(audited).map(Audited::type).orElse(Audited.NONE))) { throw new IllegalStateException("Programming error: @Audited annotation must have type set"); } audit(audited, result, signature, point.getArgs()); return result; } private void audit(Audited audited, Object result, MethodSignature signature, Object[] args) { final AuthenticationAdapter authentication = authenticationAdapterFactory.createAdapter(); final Parameter[] parameters = signature.getMethod().getParameters(); final Audited.Id auditedId = AnnotatedElementUtils.getMergedAnnotation(signature.getMethod(), Audited.Id.class); final AuditEvent auditEvent = AuditEvent.builder() .type(audited.type()) .subType(audited.subType()) .correlationId(correlationIdProvider.getCorrelationId()) .id( auditedId == null ? getIdFromFirstAnnotatedMethodParameter(parameters, args) : getIdFromAnnotatedValue("Return value", result, auditedId) ) .details(getDetailsFromAnnotatedParametersAsJson(parameters, args)) .userId(authentication.getUserId()) .username(authentication.getUsername()) .originatingIP(getClientIpAddress()) .serviceId(appName) .serviceVersion(appVersion) .serverHostName(getServerHostName()) .created(OffsetDateTime.now(ZoneOffset.UTC)) .build(); onAuditEvent(auditEvent); } private String getIdFromFirstAnnotatedMethodParameter(Parameter[] parameters, Object[] args) { for (int i = 0; i < args.length; i++) { final Audited.Id auditedId = AnnotatedElementUtils.getMergedAnnotation(parameters[i], Audited.Id.class); if (auditedId != null) { return getIdFromAnnotatedValue("Parameter " + (i + 1), args[i], auditedId); } } debug("No parameters annotated with @Audited.Id annotation"); return null; } private String getIdFromAnnotatedValue(String annotationPosition, Object value, Audited.Id auditedId) { if (value == null) { warn("{} was annotated with @Audit.Id annotation but is null", annotationPosition); return null; } if (StringUtils.isEmpty(auditedId.field())) { return value.toString(); } return readIdFromValue(annotationPosition, value, auditedId.field()); } private String readIdFromValue(String annotationPosition, Object value, String fieldName) { try { final Field field = value.getClass().getDeclaredField(fieldName); final Object fieldValue = makeAccessibleAndGetField(field, value); if (fieldValue == null) { warn("{} was annotated with @Audit.Id(field = \"{}\") annotation but the '{}' field is null", annotationPosition, fieldName, fieldName); return null; } return fieldValue.toString(); } catch (NoSuchFieldException ex) { throw new IllegalStateException( String.format( "Programming error: %s of type: %s was annotated with @Audited.Id(field = \"%s\"), but no field with the name: '%s' could " + "be found", annotationPosition, value.getClass(), fieldName, fieldName ), ex ); } } private Object makeAccessibleAndGetField(Field field, Object obj) { ReflectionUtils.makeAccessible(field); return ReflectionUtils.getField(field, obj); } private String getDetailsFromAnnotatedParametersAsJson(Parameter[] parameters, Object[] args) { final Map details = getDetailsFromAnnotatedParameters(parameters, args); if (details.isEmpty()) { return null; } return auditDetailsToJson(details); } private Map getDetailsFromAnnotatedParameters(Parameter[] parameters, Object[] args) { final Map details = new HashMap<>(); for (int i = 0; i < parameters.length; i++) { final Audited.Detail auditedDetail = AnnotatedElementUtils.getMergedAnnotation(parameters[i], Audited.Detail.class); if (auditedDetail != null) { final String detailName = auditedDetail.name(); if (StringUtils.isEmpty(detailName) && !details.isEmpty()) { throw new IllegalStateException( String.format( "Programming error: If multiple method parameters are annotated with @Audited.Detail, they must all have their name " + "set. Name was not set for parameter %d", i ) ); } details.put(StringUtils.isEmpty(detailName) ? DEFAULT_DETAILS_KEY : detailName, args[i]); } } return details; } private String auditDetailsToJson(Map details) { if (details.containsKey(DEFAULT_DETAILS_KEY)) { return GSON.toJson(details.get(DEFAULT_DETAILS_KEY)); } return GSON.toJson(details); } private String getServerHostName() { try { return localhostFacade.getServerHostName(); } catch (UnknownHostException ex) { warn("Failed to determine server host name for auditing purposes", ex); } return null; } private void onAuditEvent(AuditEvent auditEvent) { debug("Generated audit event: {}", auditEvent); try { auditEventWriter.write(auditEvent); } catch (Throwable t) { error("Failed to send audit event: {}", auditEvent); } } private void debug(String format, Object... arguments) { log(LOG::debug, format, arguments); } private void warn(String format, Object... arguments) { log(LOG::warn, format, arguments); } private void error(String format, Object... arguments) { log(LOG::error, format, arguments); } private void log(BiConsumer log, String format, Object[] arguments) { if (includeCorrelationIdInLogs) { log.accept("[{}] " + format, addCorrelationIdToArguments(correlationIdProvider.getCorrelationId(), arguments)); return; } log.accept(format, arguments); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy