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

@@ -51,4 +51,4 @@ jobs:
run: mvn license:check --batch-mode
- name: Run tests & javadoc
run: mvn verify javadoc:javadoc --batch-mode -Djpa.vendor=hibernate -Dlogging.level.org.hibernate.collection.spi.AbstractPersistentCollection=ERROR
run: mvn clean verify javadoc:javadoc --batch-mode -Djpa.vendor=hibernate -Dlogging.level.org.hibernate.collection.spi.AbstractPersistentCollection=ERROR

View File

@@ -51,4 +51,4 @@ jobs:
run: mvn license:check --batch-mode
- name: Run tests & javadoc
run: mvn verify javadoc:javadoc --batch-mode
run: mvn clean verify javadoc:javadoc --batch-mode

View File

@@ -15,6 +15,7 @@ import java.util.Map;
import java.util.stream.Collectors;
import org.eclipse.hawkbit.ContextAware;
import org.eclipse.hawkbit.audit.AuditContextProvider;
import org.eclipse.hawkbit.im.authentication.SpRole;
import org.eclipse.hawkbit.tenancy.TenantAware.DefaultTenantResolver;
import org.eclipse.hawkbit.tenancy.TenantAware.TenantResolver;
@@ -115,6 +116,12 @@ public class SecurityAutoConfiguration {
return new SpringSecurityAuditorAware();
}
@Bean
@ConditionalOnMissingBean
public AuditContextProvider auditContextProvider() {
return AuditContextProvider.getInstance();
}
/**
* @param tenantAware singleton bean
* @return tenantAware {@link SystemSecurityContext}

View File

@@ -23,6 +23,7 @@ import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
import org.eclipse.hawkbit.artifact.repository.urlhandler.ArtifactUrlHandler;
import org.eclipse.hawkbit.audit.AuditLog;
import org.eclipse.hawkbit.ddi.json.model.DdiActionFeedback;
import org.eclipse.hawkbit.ddi.json.model.DdiActionHistory;
import org.eclipse.hawkbit.ddi.json.model.DdiActivateAutoConfirmation;
@@ -451,6 +452,7 @@ public class DdiRootController implements DdiRootControllerRestApi {
}
@Override
@AuditLog(entity = "DDI", type = AuditLog.Type.UPDATE, message = "Activate Auto Confirmation", logResponse = true)
public ResponseEntity<Void> activateAutoConfirmation(
final String tenant, final String controllerId, final DdiActivateAutoConfirmation body) {
final String initiator = body == null ? null : body.getInitiator();
@@ -462,6 +464,7 @@ public class DdiRootController implements DdiRootControllerRestApi {
}
@Override
@AuditLog(entity = "DDI", type = AuditLog.Type.UPDATE, message = "Deactivate Auto Confirmation", logResponse = true)
public ResponseEntity<Void> deactivateAutoConfirmation(final String tenant, final String controllerId) {
log.debug("Deactivate auto-confirmation request for device {}", controllerId);
confirmationManagement.deactivateAutoConfirmation(controllerId);

View File

@@ -26,6 +26,7 @@ import java.util.stream.Collectors;
import jakarta.validation.constraints.NotNull;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.audit.AuditLog;
import org.eclipse.hawkbit.dmf.amqp.api.EventTopic;
import org.eclipse.hawkbit.dmf.amqp.api.MessageHeaderKey;
import org.eclipse.hawkbit.dmf.amqp.api.MessageType;
@@ -406,6 +407,7 @@ public class AmqpMessageHandlerService extends BaseAmqpService {
}
}
@AuditLog(entity = "DMF", type = AuditLog.Type.DELETE, message = "Delete Target", logResponse = true)
private void deleteTarget(final Message message) {
final String thingId = getStringHeaderKey(message, MessageHeaderKey.THING_ID, THING_ID_NULL);
controllerManagement.deleteExistingTarget(thingId);

View File

@@ -9,6 +9,7 @@
*/
package org.eclipse.hawkbit.mgmt.rest.resource;
import org.eclipse.hawkbit.audit.AuditLog;
import org.eclipse.hawkbit.mgmt.json.model.auth.MgmtUserInfo;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtBasicAuthRestApi;
import org.eclipse.hawkbit.tenancy.TenantAware;
@@ -28,6 +29,7 @@ public class MgmtBasicAuthResource implements MgmtBasicAuthRestApi {
}
@Override
@AuditLog(entity = "BasicAuth", type = AuditLog.Type.READ, message = "Validate Basic Auth")
public ResponseEntity<MgmtUserInfo> validateBasicAuth() {
final MgmtUserInfo userInfo = new MgmtUserInfo();
userInfo.setUsername(tenantAware.getCurrentUsername());

View File

@@ -21,6 +21,7 @@ import java.util.stream.Collectors;
import jakarta.validation.ValidationException;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.audit.AuditLog;
import org.eclipse.hawkbit.mgmt.json.model.MgmtMetadata;
import org.eclipse.hawkbit.mgmt.json.model.MgmtMetadataBodyPut;
import org.eclipse.hawkbit.mgmt.json.model.PagedList;
@@ -168,12 +169,14 @@ public class MgmtDistributionSetResource implements MgmtDistributionSetRestApi {
}
@Override
@AuditLog(entity = "DistributionSet", type = AuditLog.Type.DELETE, message = "Delete Distribution Set")
public ResponseEntity<Void> deleteDistributionSet(final Long distributionSetId) {
distributionSetManagement.delete(distributionSetId);
return ResponseEntity.ok().build();
}
@Override
@AuditLog(entity = "DistributionSet", type = AuditLog.Type.UPDATE, message = "Update Distribution Set")
public ResponseEntity<MgmtDistributionSet> updateDistributionSet(
final Long distributionSetId,
final MgmtDistributionSetRequestBodyPut toUpdate) {
@@ -343,6 +346,7 @@ public class MgmtDistributionSetResource implements MgmtDistributionSetRestApi {
}
@Override
@AuditLog(entity = "DistributionSet", type = AuditLog.Type.DELETE, message = "Delete Assigned Distribution Set")
public ResponseEntity<Void> deleteAssignSoftwareModules(final Long distributionSetId, final Long softwareModuleId) {
distributionSetManagement.unassignSoftwareModule(distributionSetId, softwareModuleId);
return ResponseEntity.ok().build();
@@ -396,6 +400,7 @@ public class MgmtDistributionSetResource implements MgmtDistributionSetRestApi {
}
@Override
@AuditLog(entity = "DistributionSet", type = AuditLog.Type.DELETE, message = "Invalidate Distribution Set")
public ResponseEntity<Void> invalidateDistributionSet(
final Long distributionSetId, final MgmtInvalidateDistributionSetRequestBody invalidateRequestBody) {
distributionSetInvalidationManagement

View File

@@ -12,6 +12,7 @@ package org.eclipse.hawkbit.mgmt.rest.resource;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.audit.AuditLog;
import org.eclipse.hawkbit.mgmt.json.model.PagedList;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSet;
import org.eclipse.hawkbit.mgmt.json.model.tag.MgmtTag;
@@ -99,6 +100,7 @@ public class MgmtDistributionSetTagResource implements MgmtDistributionSetTagRes
}
@Override
@AuditLog(entity = "DistributionSetTag", type = AuditLog.Type.UPDATE, message = "Update Distribution Set Tag")
public ResponseEntity<MgmtTag> updateDistributionSetTag(
final Long distributionsetTagId,
final MgmtTagRequestBodyPut restDSTagRest) {
@@ -114,6 +116,7 @@ public class MgmtDistributionSetTagResource implements MgmtDistributionSetTagRes
}
@Override
@AuditLog(entity = "DistributionSetTag", type = AuditLog.Type.DELETE, message = "Delete Distribution Set Tag")
public ResponseEntity<Void> deleteDistributionSetTag(
final Long distributionsetTagId) {
log.debug("Delete {} distribution set tag", distributionsetTagId);
@@ -169,6 +172,7 @@ public class MgmtDistributionSetTagResource implements MgmtDistributionSetTagRes
}
@Override
@AuditLog(entity = "DistributionSetTag", type = AuditLog.Type.UPDATE, message = "Unassign Distribution Set From Tag")
public ResponseEntity<Void> unassignDistributionSet(
final Long distributionsetTagId,
final Long distributionsetId) {
@@ -178,6 +182,7 @@ public class MgmtDistributionSetTagResource implements MgmtDistributionSetTagRes
}
@Override
@AuditLog(entity = "DistributionSetTag", type = AuditLog.Type.UPDATE, message = "Unassign Distribution Sets From Tag")
public ResponseEntity<Void> unassignDistributionSets(
final Long distributionsetTagId,
final List<Long> distributionsetIds) {

View File

@@ -12,6 +12,7 @@ package org.eclipse.hawkbit.mgmt.rest.resource;
import java.util.Collections;
import java.util.List;
import org.eclipse.hawkbit.audit.AuditLog;
import org.eclipse.hawkbit.mgmt.json.model.MgmtId;
import org.eclipse.hawkbit.mgmt.json.model.PagedList;
import org.eclipse.hawkbit.mgmt.json.model.distributionsettype.MgmtDistributionSetType;
@@ -87,6 +88,7 @@ public class MgmtDistributionSetTypeResource implements MgmtDistributionSetTypeR
}
@Override
@AuditLog(entity = "DistributionSetType", type = AuditLog.Type.DELETE, message = "Delete Distribution Set Type")
public ResponseEntity<Void> deleteDistributionSetType(final Long distributionSetTypeId) {
distributionSetTypeManagement.delete(distributionSetTypeId);
@@ -94,6 +96,7 @@ public class MgmtDistributionSetTypeResource implements MgmtDistributionSetTypeR
}
@Override
@AuditLog(entity = "DistributionSetType", type = AuditLog.Type.UPDATE, message = "Update Distribution Set Type")
public ResponseEntity<MgmtDistributionSetType> updateDistributionSetType(
final Long distributionSetTypeId, final MgmtDistributionSetTypeRequestBodyPut restDistributionSetType) {
final DistributionSetType updated = distributionSetTypeManagement.update(entityFactory.distributionSetType()
@@ -153,6 +156,7 @@ public class MgmtDistributionSetTypeResource implements MgmtDistributionSetTypeR
}
@Override
@AuditLog(entity = "DistributionSetType", type = AuditLog.Type.UPDATE, message = "Remove Mandatory Module From Distribution Set Type")
public ResponseEntity<Void> removeMandatoryModule(final Long distributionSetTypeId, final Long softwareModuleTypeId) {
distributionSetTypeManagement.unassignSoftwareModuleType(distributionSetTypeId, softwareModuleTypeId);
return ResponseEntity.ok().build();
@@ -164,6 +168,7 @@ public class MgmtDistributionSetTypeResource implements MgmtDistributionSetTypeR
}
@Override
@AuditLog(entity = "DistributionSetType", type = AuditLog.Type.UPDATE, message = "Add Mandatory Module From Distribution Set Type")
public ResponseEntity<Void> addMandatoryModule(final Long distributionSetTypeId, final MgmtId smtId) {
distributionSetTypeManagement.assignMandatorySoftwareModuleTypes(distributionSetTypeId, Collections.singletonList(smtId.getId()));
return ResponseEntity.ok().build();

View File

@@ -25,7 +25,6 @@ import org.eclipse.hawkbit.repository.model.SoftwareModule;
import org.eclipse.hawkbit.rest.util.FileStreamingUtil;
import org.eclipse.hawkbit.rest.util.HttpUtil;
import org.eclipse.hawkbit.rest.util.RequestResponseContextHolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;

View File

@@ -16,6 +16,7 @@ import java.util.Optional;
import jakarta.validation.ValidationException;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.audit.AuditLog;
import org.eclipse.hawkbit.mgmt.json.model.PagedList;
import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutResponseBody;
import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutRestRequestBodyPost;
@@ -180,24 +181,28 @@ public class MgmtRolloutResource implements MgmtRolloutRestApi {
}
@Override
@AuditLog(entity = "Rollout", type = AuditLog.Type.UPDATE, message = "Start Rollout")
public ResponseEntity<Void> start(final Long rolloutId) {
this.rolloutManagement.start(rolloutId);
return ResponseEntity.ok().build();
}
@Override
@AuditLog(entity = "Rollout", type = AuditLog.Type.UPDATE, message = "Pause Rollout")
public ResponseEntity<Void> pause(final Long rolloutId) {
this.rolloutManagement.pauseRollout(rolloutId);
return ResponseEntity.ok().build();
}
@Override
@AuditLog(entity = "Rollout", type = AuditLog.Type.DELETE, message = "Delete Rollout")
public ResponseEntity<Void> delete(final Long rolloutId) {
this.rolloutManagement.delete(rolloutId);
return ResponseEntity.ok().build();
}
@Override
@AuditLog(entity = "Rollout", type = AuditLog.Type.UPDATE, message = "Resume Rollout")
public ResponseEntity<Void> resume(final Long rolloutId) {
this.rolloutManagement.resumeRollout(rolloutId);
return ResponseEntity.ok().build();
@@ -276,6 +281,7 @@ public class MgmtRolloutResource implements MgmtRolloutRestApi {
}
@Override
@AuditLog(entity = "Rollout", type = AuditLog.Type.UPDATE, message = "Trigger Next Rollout Group")
public ResponseEntity<Void> triggerNextGroup(final Long rolloutId) {
this.rolloutManagement.triggerNextGroup(rolloutId);
return ResponseEntity.ok().build();

View File

@@ -20,6 +20,7 @@ import jakarta.validation.ValidationException;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.artifact.repository.urlhandler.ArtifactUrlHandler;
import org.eclipse.hawkbit.audit.AuditLog;
import org.eclipse.hawkbit.mgmt.json.model.PagedList;
import org.eclipse.hawkbit.mgmt.json.model.artifact.MgmtArtifact;
import org.eclipse.hawkbit.mgmt.json.model.softwaremodule.MgmtSoftwareModule;
@@ -146,6 +147,7 @@ public class MgmtSoftwareModuleResource implements MgmtSoftwareModuleRestApi {
}
@Override
@AuditLog(entity = "SoftwareModule", type = AuditLog.Type.DELETE, message = "Delete Software Module Artifact")
public ResponseEntity<Void> deleteArtifact(final Long softwareModuleId, final Long artifactId) {
findSoftwareModuleWithExceptionIfNotFound(softwareModuleId, artifactId);
artifactManagement.delete(artifactId);
@@ -223,6 +225,7 @@ public class MgmtSoftwareModuleResource implements MgmtSoftwareModuleRestApi {
}
@Override
@AuditLog(entity = "SoftwareModule", type = AuditLog.Type.DELETE, message = "Delete Software Module")
public ResponseEntity<Void> deleteSoftwareModule(final Long softwareModuleId) {
final SoftwareModule module = findSoftwareModuleWithExceptionIfNotFound(softwareModuleId, null);
softwareModuleManagement.delete(module.getId());
@@ -274,6 +277,7 @@ public class MgmtSoftwareModuleResource implements MgmtSoftwareModuleRestApi {
}
@Override
@AuditLog(entity = "SoftwareModule", type = AuditLog.Type.DELETE, message = "Delete Software Module Metadata")
public ResponseEntity<Void> deleteMetadata(final Long softwareModuleId, final String metadataKey) {
softwareModuleManagement.deleteMetaData(softwareModuleId, metadataKey);
return ResponseEntity.ok().build();

View File

@@ -11,6 +11,7 @@ package org.eclipse.hawkbit.mgmt.rest.resource;
import java.util.List;
import org.eclipse.hawkbit.audit.AuditLog;
import org.eclipse.hawkbit.mgmt.json.model.PagedList;
import org.eclipse.hawkbit.mgmt.json.model.softwaremoduletype.MgmtSoftwareModuleType;
import org.eclipse.hawkbit.mgmt.json.model.softwaremoduletype.MgmtSoftwareModuleTypeRequestBodyPost;
@@ -74,6 +75,7 @@ public class MgmtSoftwareModuleTypeResource implements MgmtSoftwareModuleTypeRes
}
@Override
@AuditLog(entity = "SoftwareModuleType", type = AuditLog.Type.DELETE, message = "Delete Software Module Type")
public ResponseEntity<Void> deleteSoftwareModuleType(final Long softwareModuleTypeId) {
softwareModuleTypeManagement.delete(softwareModuleTypeId);
return ResponseEntity.ok().build();

View File

@@ -12,7 +12,6 @@ package org.eclipse.hawkbit.mgmt.rest.resource;
import java.util.Collection;
import java.util.Collections;
import java.util.Objects;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions;

View File

@@ -12,6 +12,7 @@ package org.eclipse.hawkbit.mgmt.rest.resource;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.audit.AuditLog;
import org.eclipse.hawkbit.mgmt.json.model.PagedList;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSet;
import org.eclipse.hawkbit.mgmt.json.model.targetfilter.MgmtDistributionSetAutoAssignment;
@@ -127,6 +128,7 @@ public class MgmtTargetFilterQueryResource implements MgmtTargetFilterQueryRestA
}
@Override
@AuditLog(entity = "TargetFilter", type = AuditLog.Type.DELETE, message = "Delete Target Filter")
public ResponseEntity<Void> deleteFilter(final Long filterId) {
filterManagement.delete(filterId);
log.debug("{} target filter query deleted, return status {}", filterId, HttpStatus.OK);
@@ -169,6 +171,7 @@ public class MgmtTargetFilterQueryResource implements MgmtTargetFilterQueryRestA
}
@Override
@AuditLog(entity = "TargetFilter", type = AuditLog.Type.DELETE, message = "Delete Target Filter Assigned Distribution Set")
public ResponseEntity<Void> deleteAssignedDistributionSet(final Long filterId) {
filterManagement.updateAutoAssignDS(entityFactory.targetFilterQuery().updateAutoAssign(filterId).ds(null));
return ResponseEntity.noContent().build();

View File

@@ -23,6 +23,7 @@ import java.util.stream.Collectors;
import jakarta.validation.ValidationException;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.audit.AuditLog;
import org.eclipse.hawkbit.mgmt.json.model.MgmtId;
import org.eclipse.hawkbit.mgmt.json.model.MgmtMetadata;
import org.eclipse.hawkbit.mgmt.json.model.MgmtMetadataBodyPut;
@@ -138,6 +139,7 @@ public class MgmtTargetResource implements MgmtTargetRestApi {
}
@Override
@AuditLog(entity = "Target", type = AuditLog.Type.UPDATE, message = "Update Target")
public ResponseEntity<MgmtTarget> updateTarget(final String targetId, final MgmtTargetRequestBody targetRest) {
if (targetRest.getRequestAttributes() != null) {
if (Boolean.TRUE.equals(targetRest.getRequestAttributes())) {
@@ -172,6 +174,7 @@ public class MgmtTargetResource implements MgmtTargetRestApi {
}
@Override
@AuditLog(entity = "Target", type = AuditLog.Type.DELETE, message = "Delete Target")
public ResponseEntity<Void> deleteTarget(final String targetId) {
this.targetManagement.deleteByControllerID(targetId);
log.debug("{} target deleted, return status {}", targetId, HttpStatus.OK);
@@ -179,12 +182,14 @@ public class MgmtTargetResource implements MgmtTargetRestApi {
}
@Override
@AuditLog(entity = "Target", type = AuditLog.Type.UPDATE, message = "Unassign Target Type")
public ResponseEntity<Void> unassignTargetType(final String targetId) {
this.targetManagement.unassignType(targetId);
return ResponseEntity.ok().build();
}
@Override
@AuditLog(entity = "Target", type = AuditLog.Type.UPDATE, message = "Assign Target")
public ResponseEntity<Void> assignTargetType(final String targetId, final MgmtId targetTypeId) {
this.targetManagement.assignType(targetId, targetTypeId.getId());
return ResponseEntity.ok().build();
@@ -245,6 +250,7 @@ public class MgmtTargetResource implements MgmtTargetRestApi {
}
@Override
@AuditLog(entity = "Target", type = AuditLog.Type.UPDATE, message = "Cancel Target Action")
public ResponseEntity<Void> cancelAction(final String targetId, final Long actionId, final boolean force) {
final Action action = deploymentManagement.findAction(actionId)
.orElseThrow(() -> new EntityNotFoundException(Action.class, actionId));
@@ -266,6 +272,7 @@ public class MgmtTargetResource implements MgmtTargetRestApi {
}
@Override
@AuditLog(entity = "Target", type = AuditLog.Type.UPDATE, message = "Update Target Action")
public ResponseEntity<MgmtAction> updateAction(final String targetId, final Long actionId, final MgmtActionRequestBodyPut actionUpdate) {
Action action = deploymentManagement.findAction(actionId)
@@ -285,6 +292,7 @@ public class MgmtTargetResource implements MgmtTargetRestApi {
}
@Override
@AuditLog(entity = "Target", type = AuditLog.Type.UPDATE, message = "Cancel Target Action Confirmation")
public ResponseEntity<Void> updateActionConfirmation(final String targetId, final Long actionId,
final MgmtActionConfirmationRequestBodyPut actionConfirmation) {
log.debug("updateActionConfirmation with data [targetId={}, actionId={}]: {}", targetId, actionId, actionConfirmation);
@@ -369,6 +377,7 @@ public class MgmtTargetResource implements MgmtTargetRestApi {
}
@Override
@AuditLog(entity = "Target", type = AuditLog.Type.UPDATE, message = "Set Distribution Set To Target")
public ResponseEntity<MgmtTargetAssignmentResponseBody> postAssignedDistributionSet(
final String targetId, final MgmtDistributionSetAssignments dsAssignments, final Boolean offline) {
if (offline != null && offline) {
@@ -452,6 +461,7 @@ public class MgmtTargetResource implements MgmtTargetRestApi {
}
@Override
@AuditLog(entity = "Target", type = AuditLog.Type.DELETE, message = "Delete Target Metadata")
public ResponseEntity<Void> deleteMetadata(final String targetId, final String metadataKey) {
targetManagement.deleteMetaData(targetId, metadataKey);
return ResponseEntity.ok().build();
@@ -472,6 +482,7 @@ public class MgmtTargetResource implements MgmtTargetRestApi {
}
@Override
@AuditLog(entity = "Target", type = AuditLog.Type.UPDATE, message = "Activate Target Auto Confirmation")
public ResponseEntity<Void> activateAutoConfirm(final String targetId, final MgmtTargetAutoConfirmUpdate update) {
final String initiator = getNullIfEmpty(update, MgmtTargetAutoConfirmUpdate::getInitiator);
final String remark = getNullIfEmpty(update, MgmtTargetAutoConfirmUpdate::getRemark);
@@ -480,6 +491,7 @@ public class MgmtTargetResource implements MgmtTargetRestApi {
}
@Override
@AuditLog(entity = "Target", type = AuditLog.Type.UPDATE, message = "Deactivate Target Auto Confirmation")
public ResponseEntity<Void> deactivateAutoConfirm(final String targetId) {
confirmationManagement.deactivateAutoConfirmation(targetId);
return new ResponseEntity<>(HttpStatus.OK);

View File

@@ -14,6 +14,7 @@ import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.audit.AuditLog;
import org.eclipse.hawkbit.mgmt.json.model.PagedList;
import org.eclipse.hawkbit.mgmt.json.model.tag.MgmtTag;
import org.eclipse.hawkbit.mgmt.json.model.tag.MgmtTagRequestBodyPut;
@@ -112,6 +113,7 @@ public class MgmtTargetTagResource implements MgmtTargetTagRestApi {
}
@Override
@AuditLog(entity = "TargetTag", type = AuditLog.Type.DELETE, message = "Delete Target Tag")
public ResponseEntity<Void> deleteTargetTag(final Long targetTagId) {
log.debug("Delete {} target tag", targetTagId);
final TargetTag targetTag = findTargetTagById(targetTagId);
@@ -166,6 +168,7 @@ public class MgmtTargetTagResource implements MgmtTargetTagRestApi {
}
@Override
@AuditLog(entity = "TargetTag", type = AuditLog.Type.UPDATE, message = "Unassign Target From Target Tag")
public ResponseEntity<Void> unassignTarget(final Long targetTagId, final String controllerId) {
log.debug("Unassign target {} for target tag {}", controllerId, targetTagId);
this.targetManagement.unassignTag(List.of(controllerId), targetTagId);
@@ -173,6 +176,7 @@ public class MgmtTargetTagResource implements MgmtTargetTagRestApi {
}
@Override
@AuditLog(entity = "TargetTag", type = AuditLog.Type.UPDATE, message = "Unassign Targets From Target Tag")
public ResponseEntity<Void> unassignTargets(
final Long targetTagId, final OnNotFoundPolicy onNotFoundPolicy, final List<String> controllerIds) {
log.debug("Unassign {} targets for target tag {}", controllerIds.size(), targetTagId);

View File

@@ -12,6 +12,7 @@ package org.eclipse.hawkbit.mgmt.rest.resource;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.audit.AuditLog;
import org.eclipse.hawkbit.mgmt.json.model.PagedList;
import org.eclipse.hawkbit.mgmt.json.model.distributionsettype.MgmtDistributionSetType;
import org.eclipse.hawkbit.mgmt.json.model.distributionsettype.MgmtDistributionSetTypeAssignment;
@@ -79,6 +80,7 @@ public class MgmtTargetTypeResource implements MgmtTargetTypeRestApi {
}
@Override
@AuditLog(entity = "TargetType", type = AuditLog.Type.DELETE, message = "Delete Target Type")
public ResponseEntity<Void> deleteTargetType(final Long targetTypeId) {
log.debug("Delete {} target type", targetTypeId);
targetTypeManagement.delete(targetTypeId);
@@ -109,12 +111,14 @@ public class MgmtTargetTypeResource implements MgmtTargetTypeRestApi {
}
@Override
@AuditLog(entity = "TargetType", type = AuditLog.Type.DELETE, message = "Remove Compatible Distribution Set From Target Type")
public ResponseEntity<Void> removeCompatibleDistributionSet(final Long targetTypeId, final Long distributionSetTypeId) {
targetTypeManagement.unassignDistributionSetType(targetTypeId, distributionSetTypeId);
return ResponseEntity.ok().build();
}
@Override
@AuditLog(entity = "TargetType", type = AuditLog.Type.UPDATE, message = "Add Compatible Distribution Set To Target Type")
public ResponseEntity<Void> addCompatibleDistributionSets(
final Long targetTypeId, final List<MgmtDistributionSetTypeAssignment> distributionSetTypeIds) {
targetTypeManagement.assignCompatibleDistributionSetTypes(

View File

@@ -16,6 +16,7 @@ import java.util.Map;
import java.util.Objects;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.audit.AuditLog;
import org.eclipse.hawkbit.mgmt.json.model.system.MgmtSystemTenantConfigurationValue;
import org.eclipse.hawkbit.mgmt.json.model.system.MgmtSystemTenantConfigurationValueRequest;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtTenantManagementRestApi;
@@ -72,6 +73,7 @@ public class MgmtTenantManagementResource implements MgmtTenantManagementRestApi
}
@Override
@AuditLog(entity = "TenantConfiguration", type = AuditLog.Type.DELETE, message = "Delete Tenant Configuration Value")
public ResponseEntity<Void> deleteTenantConfigurationValue(final String keyName) {
//Default DistributionSet Type cannot be deleted as is part of TenantMetadata
if (isDefaultDistributionSetTypeKey(keyName)) {
@@ -90,6 +92,7 @@ public class MgmtTenantManagementResource implements MgmtTenantManagementRestApi
}
@Override
@AuditLog(entity = "TenantConfiguration", type = AuditLog.Type.UPDATE, message = "Update Tenant Configuration Value")
public ResponseEntity<MgmtSystemTenantConfigurationValue> updateTenantConfigurationValue(
final String keyName, final MgmtSystemTenantConfigurationValueRequest configurationValueRest) {
Serializable configurationValue = configurationValueRest.getValue();
@@ -106,6 +109,7 @@ public class MgmtTenantManagementResource implements MgmtTenantManagementRestApi
}
@Override
@AuditLog(entity = "TenantConfiguration", type = AuditLog.Type.UPDATE, message = "Update Tenant Configuration")
public ResponseEntity<List<MgmtSystemTenantConfigurationValue>> updateTenantConfiguration(
final Map<String, Serializable> configurationValueMap) {
final boolean containsNull = configurationValueMap.keySet().stream().anyMatch(Objects::isNull);

View File

@@ -36,6 +36,10 @@
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-aspects</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>

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