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:
Denislav Prinov
2025-03-31 08:51:54 +03:00
committed by GitHub
parent bbc725d6a7
commit 23154d70cc
24 changed files with 367 additions and 4 deletions

View File

@@ -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) {}
}

View File

@@ -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;
}

View File

@@ -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;
}
}
}

View File

@@ -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) {}
}