Audit Logging in HawkBit (#2314)
* Introduction of Audit Logging in hawkBit Signed-off-by: Denislav Prinov <denislav.prinov@bosch.com> * Introduction of Audit Logging in hawkBit Signed-off-by: Denislav Prinov <denislav.prinov@bosch.com> * Refactoring: * applied code formatter * audit moved into hawkbit-security-core * minimize dependences * use AuditorAware to retrieve user - so to be compatible with the logs into DB Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com> * Move audit entities to security core Signed-off-by: Denislav Prinov <denislav.prinov@bosch.com> * Introduce audit log method types Signed-off-by: Denislav Prinov <denislav.prinov@bosch.com> --------- Signed-off-by: Denislav Prinov <denislav.prinov@bosch.com> Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com> Co-authored-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.audit;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.eclipse.hawkbit.tenancy.TenantAware;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.AuditorAware;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
||||
@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE)
|
||||
@SuppressWarnings("java:S6548") // java:S6548 - singleton holder ensures static access to spring resources in some places
|
||||
public class AuditContextProvider {
|
||||
|
||||
private static final AuditContextProvider INSTANCE = new AuditContextProvider();
|
||||
|
||||
private TenantAware.TenantResolver resolver;
|
||||
private AuditorAware<String> auditorAware;
|
||||
|
||||
public static AuditContextProvider getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void setTenantResolver(final TenantAware.TenantResolver resolver) {
|
||||
this.resolver = resolver;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void setAuditorAware(final AuditorAware<String> auditorAware) {
|
||||
this.auditorAware = auditorAware;
|
||||
}
|
||||
|
||||
public AuditContext getAuditContext() {
|
||||
return new AuditContext(
|
||||
Optional.ofNullable(resolver.resolveTenant()).orElse("n/a"),
|
||||
Optional.ofNullable(auditorAware).flatMap(AuditorAware::getCurrentAuditor).orElse("system"));
|
||||
}
|
||||
|
||||
public record AuditContext(String tenant, String username) {}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.audit;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
public @interface AuditLog {
|
||||
|
||||
enum Level {
|
||||
INFO, WARN, ERROR
|
||||
}
|
||||
|
||||
enum Type {
|
||||
CREATE, READ, UPDATE, DELETE, EXECUTE
|
||||
}
|
||||
|
||||
Level level() default Level.INFO;
|
||||
|
||||
Type type();
|
||||
|
||||
String entity();
|
||||
|
||||
String message() default "";
|
||||
|
||||
String[] logParams() default {};
|
||||
|
||||
boolean logResponse() default false;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.audit;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class AuditLogger {
|
||||
|
||||
private static final AuditContextProvider AUDIT_CONTEXT_PROVIDER = AuditContextProvider.getInstance();
|
||||
|
||||
public static void info(final String entity, final String message) {
|
||||
logMessage(entity, message, AuditLog.Level.INFO);
|
||||
}
|
||||
|
||||
public static void info(final String tenant, final String username, final String entity, final String message) {
|
||||
logMessage(tenant, username, entity, message, AuditLog.Level.INFO);
|
||||
}
|
||||
|
||||
public static void error(final String entity, final String message) {
|
||||
logMessage(entity, message, AuditLog.Level.ERROR);
|
||||
}
|
||||
|
||||
public static void error(final String tenant, final String username, final String entity, final String message) {
|
||||
logMessage(tenant, username, entity, message, AuditLog.Level.ERROR);
|
||||
}
|
||||
|
||||
public static void warn(final String entity, final String message) {
|
||||
logMessage(entity, message, AuditLog.Level.WARN);
|
||||
}
|
||||
|
||||
public static void warn(final String tenant, final String username, final String entity, final String message) {
|
||||
logMessage(tenant, username, entity, message, AuditLog.Level.WARN);
|
||||
}
|
||||
|
||||
private static void logMessage(final String entity, final String message, final AuditLog.Level level) {
|
||||
logMessage(AUDIT_CONTEXT_PROVIDER.getAuditContext().tenant(), AUDIT_CONTEXT_PROVIDER.getAuditContext().username(), entity, message,
|
||||
level);
|
||||
}
|
||||
|
||||
private static void logMessage(
|
||||
final String tenant, final String username, final String entity, final String message, final AuditLog.Level level) {
|
||||
final String logMessage = String.format("[%s] User: %s, Tenant: %s - %s", entity, username, tenant, message);
|
||||
final Logger auditLogger = LoggerFactory.getLogger("AUDIT" + (entity != null ? ("." + entity.toUpperCase()) : ""));
|
||||
switch (level) {
|
||||
case INFO:
|
||||
auditLogger.info(logMessage);
|
||||
break;
|
||||
case WARN:
|
||||
auditLogger.warn(logMessage);
|
||||
break;
|
||||
case ERROR:
|
||||
auditLogger.error(logMessage);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.audit;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
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.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Slf4j
|
||||
@Aspect
|
||||
@Component
|
||||
public class AuditLoggingAspect {
|
||||
|
||||
/**
|
||||
* Around advice that applies to methods annotated with @AuditLog.
|
||||
* It logs the request and, if logResponse is true, the response as well.
|
||||
*/
|
||||
@Around("@annotation(auditLog)")
|
||||
public Object handleAuditLogging(final ProceedingJoinPoint joinPoint, final AuditLog auditLog) throws Throwable {
|
||||
try {
|
||||
final Object result = joinPoint.proceed();
|
||||
try { // log success
|
||||
final ResultMessage resultMessage = getResultMessage(result, auditLog);
|
||||
final String paramsToLog = getParamsToLog(joinPoint, auditLog);
|
||||
logAudit(joinPoint, auditLog, resultMessage.message(), paramsToLog, resultMessage.level());
|
||||
} catch (final Throwable logError) {
|
||||
// should never fail!
|
||||
log.debug("Failed to log success", logError);
|
||||
}
|
||||
return result;
|
||||
} catch (final Throwable t) {
|
||||
try {
|
||||
final String paramsToLog = getParamsToLog(joinPoint, auditLog);
|
||||
logAudit(joinPoint, auditLog, t.getMessage(), paramsToLog, AuditLog.Level.ERROR);
|
||||
} catch (final Throwable logError) {
|
||||
// should never fail!
|
||||
log.debug("Failed to log error", logError);
|
||||
}
|
||||
throw t;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs both the request details and the response.
|
||||
*/
|
||||
private void logAudit(
|
||||
final JoinPoint joinPoint, final AuditLog auditLog, final String resultMessage, final String paramsToLog,
|
||||
final AuditLog.Level logLevel) {
|
||||
final String methodName = joinPoint.getSignature().getName();
|
||||
|
||||
final String logMessage = String.format(
|
||||
"Type: %s, Method: %s - Message: %s - Parameters: %s - Response: %s",
|
||||
auditLog.type(), methodName, auditLog.message(), paramsToLog, resultMessage
|
||||
);
|
||||
|
||||
switch (logLevel) {
|
||||
case INFO:
|
||||
AuditLogger.info(auditLog.entity(), logMessage);
|
||||
break;
|
||||
case WARN:
|
||||
AuditLogger.warn(auditLog.entity(), logMessage);
|
||||
break;
|
||||
case ERROR:
|
||||
AuditLogger.error(auditLog.entity(), logMessage);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private String getParamsToLog(final JoinPoint joinPoint, final AuditLog auditLog) {
|
||||
final Object[] args = joinPoint.getArgs();
|
||||
final String[] includeParams = auditLog.logParams();
|
||||
|
||||
if (includeParams.length == 0) {
|
||||
return Arrays.deepToString(args);
|
||||
} else {
|
||||
final MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
|
||||
final String[] paramNames = methodSignature.getParameterNames();
|
||||
final Map<String, Object> paramMap = IntStream.range(0, paramNames.length)
|
||||
.boxed()
|
||||
.collect(Collectors.toMap(i -> paramNames[i], i -> args[i]));
|
||||
|
||||
return Arrays.stream(includeParams)
|
||||
.filter(paramMap::containsKey)
|
||||
.map(name -> name + "=" + paramMap.get(name))
|
||||
.collect(Collectors.joining(", "));
|
||||
}
|
||||
}
|
||||
|
||||
private ResultMessage getResultMessage(final Object result, final AuditLog auditLog) {
|
||||
final ResultMessage.ResultMessageBuilder resultMessageBuilder = ResultMessage.builder();
|
||||
if (result instanceof ResponseEntity<?> responseEntity) {
|
||||
int statusCode = responseEntity.getStatusCode().value();
|
||||
if (statusCode >= 200 && statusCode < 300) {
|
||||
resultMessageBuilder.level(AuditLog.Level.INFO);
|
||||
if (auditLog.logResponse()) {
|
||||
resultMessageBuilder.message(result.toString());
|
||||
} else {
|
||||
resultMessageBuilder.message("OK - " + statusCode);
|
||||
}
|
||||
} else {
|
||||
resultMessageBuilder.level(AuditLog.Level.WARN);
|
||||
if (auditLog.logResponse()) {
|
||||
resultMessageBuilder.message(result.toString());
|
||||
} else {
|
||||
resultMessageBuilder.message("FAILED - " + statusCode);
|
||||
}
|
||||
}
|
||||
return resultMessageBuilder.build();
|
||||
}
|
||||
resultMessageBuilder.message(result != null ? result.toString() : "null");
|
||||
resultMessageBuilder.level(auditLog.level());
|
||||
return resultMessageBuilder.build();
|
||||
}
|
||||
|
||||
@Builder
|
||||
private record ResultMessage(String message, AuditLog.Level level) {}
|
||||
}
|
||||
Reference in New Issue
Block a user