This is an automated email from the ASF dual-hosted git repository. lahirujayathilake pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/airavata-custos.git
commit 006377e79a710b6a9bfc96695cec8995a40669af Author: lahiruj <[email protected]> AuthorDate: Fri Mar 27 12:05:26 2026 -0400 add audit log table and service for handler actions tracking --- .../access/ci/service/model/amie/AuditAction.java | 39 ++++++ .../ci/service/model/amie/AuditLogEntity.java | 130 ++++++++++++++++++ .../ci/service/repo/amie/AuditLogRepository.java | 31 +++++ .../access/ci/service/service/AuditService.java | 86 ++++++++++++ .../db/migration/V3__create_audit_log.sql | 36 +++++ .../ci/service/service/AuditServiceTest.java | 152 +++++++++++++++++++++ 6 files changed, 474 insertions(+) diff --git a/allocations/access-ci-service/src/main/java/org/apache/custos/access/ci/service/model/amie/AuditAction.java b/allocations/access-ci-service/src/main/java/org/apache/custos/access/ci/service/model/amie/AuditAction.java new file mode 100644 index 000000000..81f18674a --- /dev/null +++ b/allocations/access-ci-service/src/main/java/org/apache/custos/access/ci/service/model/amie/AuditAction.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.custos.access.ci.service.model.amie; + +/** + * Enum of auditable actions performed during AMIE packet processing. + */ +public enum AuditAction { + CREATE_PERSON, + UPDATE_PERSON, + DELETE_PERSON, + MERGE_PERSONS, + CREATE_ACCOUNT, + CREATE_PROJECT, + INACTIVATE_PROJECT, + REACTIVATE_PROJECT, + CREATE_MEMBERSHIP, + INACTIVATE_MEMBERSHIP, + REACTIVATE_MEMBERSHIP, + PERSIST_DNS, + REPLY_SENT, + TRANSACTION_COMPLETE +} diff --git a/allocations/access-ci-service/src/main/java/org/apache/custos/access/ci/service/model/amie/AuditLogEntity.java b/allocations/access-ci-service/src/main/java/org/apache/custos/access/ci/service/model/amie/AuditLogEntity.java new file mode 100644 index 000000000..4f6b27929 --- /dev/null +++ b/allocations/access-ci-service/src/main/java/org/apache/custos/access/ci/service/model/amie/AuditLogEntity.java @@ -0,0 +1,130 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.custos.access.ci.service.model.amie; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; + +import java.time.Instant; + +@Entity +@Table(name = "amie_audit_log") +public class AuditLogEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "packet_id", nullable = false) + private PacketEntity packet; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "event_id") + private ProcessingEventEntity event; + + @Enumerated(EnumType.STRING) + @Column(name = "action", columnDefinition = "VARCHAR(64)", nullable = false) + private AuditAction action; + + @Column(name = "entity_type", nullable = false, length = 64) + private String entityType; + + @Column(name = "entity_id", length = 255) + private String entityId; + + @Column(name = "summary", columnDefinition = "TEXT") + private String summary; + + @Column(name = "created_at", nullable = false) + private Instant createdAt = Instant.now(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public PacketEntity getPacket() { + return packet; + } + + public void setPacket(PacketEntity packet) { + this.packet = packet; + } + + public ProcessingEventEntity getEvent() { + return event; + } + + public void setEvent(ProcessingEventEntity event) { + this.event = event; + } + + public AuditAction getAction() { + return action; + } + + public void setAction(AuditAction action) { + this.action = action; + } + + public String getEntityType() { + return entityType; + } + + public void setEntityType(String entityType) { + this.entityType = entityType; + } + + public String getEntityId() { + return entityId; + } + + public void setEntityId(String entityId) { + this.entityId = entityId; + } + + public String getSummary() { + return summary; + } + + public void setSummary(String summary) { + this.summary = summary; + } + + public Instant getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; + } +} diff --git a/allocations/access-ci-service/src/main/java/org/apache/custos/access/ci/service/repo/amie/AuditLogRepository.java b/allocations/access-ci-service/src/main/java/org/apache/custos/access/ci/service/repo/amie/AuditLogRepository.java new file mode 100644 index 000000000..f7d8e3a31 --- /dev/null +++ b/allocations/access-ci-service/src/main/java/org/apache/custos/access/ci/service/repo/amie/AuditLogRepository.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.custos.access.ci.service.repo.amie; + +import org.apache.custos.access.ci.service.model.amie.AuditLogEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface AuditLogRepository extends JpaRepository<AuditLogEntity, Long> { + + List<AuditLogEntity> findByPacketIdOrderByCreatedAtAsc(String packetId); +} diff --git a/allocations/access-ci-service/src/main/java/org/apache/custos/access/ci/service/service/AuditService.java b/allocations/access-ci-service/src/main/java/org/apache/custos/access/ci/service/service/AuditService.java new file mode 100644 index 000000000..670ce4c0e --- /dev/null +++ b/allocations/access-ci-service/src/main/java/org/apache/custos/access/ci/service/service/AuditService.java @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.custos.access.ci.service.service; + +import org.apache.custos.access.ci.service.model.amie.AuditAction; +import org.apache.custos.access.ci.service.model.amie.AuditLogEntity; +import org.apache.custos.access.ci.service.repo.amie.AuditLogRepository; +import org.apache.custos.access.ci.service.repo.amie.PacketRepository; +import org.apache.custos.access.ci.service.repo.amie.ProcessingEventRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +/** + * Service for recording audit log entries during AMIE packet processing. + * + * <p>This service is not annotated with {@code @Transactional}, + * so that this be in the caller's active transaction without introducing a new one. + * There must be an active transaction when invoking {@link #log}. + */ +@Service +public class AuditService { + + private static final Logger LOGGER = LoggerFactory.getLogger(AuditService.class); + + private final AuditLogRepository auditLogRepository; + private final PacketRepository packetRepository; + private final ProcessingEventRepository processingEventRepository; + + public AuditService(AuditLogRepository auditLogRepository, + PacketRepository packetRepository, + ProcessingEventRepository processingEventRepository) { + this.auditLogRepository = auditLogRepository; + this.packetRepository = packetRepository; + this.processingEventRepository = processingEventRepository; + } + + /** + * Records an audit log entry for an AMIE packet processing action. + * + * @param packetId the ID of the packet being processed (not null) + * @param eventId the ID of the processing event, or null if not associated with an event + * @param action the auditable action performed + * @param entityType a label describing the type of entity affected (e.g., "Person", "Project") + * @param entityId the ID of the affected entity, or null if not applicable + * @param summary a human-readable description of what happened, or null + */ + public void log(String packetId, + String eventId, + AuditAction action, + String entityType, + String entityId, + String summary) { + + AuditLogEntity entry = new AuditLogEntity(); + entry.setPacket(packetRepository.getReferenceById(packetId)); + if (eventId != null) { + entry.setEvent(processingEventRepository.getReferenceById(eventId)); + } + entry.setAction(action); + entry.setEntityType(entityType); + entry.setEntityId(entityId); + entry.setSummary(summary); + + auditLogRepository.save(entry); + + LOGGER.debug("Audit entry recorded: packetId={}, eventId={}, action={}, entityType={}, entityId={}", + packetId, eventId, action, entityType, entityId); + } +} diff --git a/allocations/access-ci-service/src/main/resources/db/migration/V3__create_audit_log.sql b/allocations/access-ci-service/src/main/resources/db/migration/V3__create_audit_log.sql new file mode 100644 index 000000000..6c10ad7e6 --- /dev/null +++ b/allocations/access-ci-service/src/main/resources/db/migration/V3__create_audit_log.sql @@ -0,0 +1,36 @@ +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You under the Apache License, Version 2.0 +-- (the "License"); you may not use this file except in compliance with +-- the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +-- Audit log for handler actions during AMIE packet processing. + +CREATE TABLE amie_audit_log +( + id BIGINT NOT NULL AUTO_INCREMENT, + packet_id VARCHAR(255) NOT NULL, + event_id VARCHAR(255) NULL, + action VARCHAR(64) NOT NULL, + entity_type VARCHAR(64) NOT NULL, + entity_id VARCHAR(255) NULL, + summary TEXT NULL, + created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + PRIMARY KEY (id), + CONSTRAINT fk_audit_packet FOREIGN KEY (packet_id) REFERENCES amie_packets (id) ON DELETE CASCADE, + CONSTRAINT fk_audit_event FOREIGN KEY (event_id) REFERENCES amie_processing_events (id) ON DELETE SET NULL, + KEY idx_audit_packet_id (packet_id), + KEY idx_audit_action (action), + KEY idx_audit_created_at (created_at) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_unicode_ci; diff --git a/allocations/access-ci-service/src/test/java/org/apache/custos/access/ci/service/service/AuditServiceTest.java b/allocations/access-ci-service/src/test/java/org/apache/custos/access/ci/service/service/AuditServiceTest.java new file mode 100644 index 000000000..0df55e7fa --- /dev/null +++ b/allocations/access-ci-service/src/test/java/org/apache/custos/access/ci/service/service/AuditServiceTest.java @@ -0,0 +1,152 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.custos.access.ci.service.service; + +import org.apache.custos.access.ci.service.model.amie.AuditAction; +import org.apache.custos.access.ci.service.model.amie.AuditLogEntity; +import org.apache.custos.access.ci.service.model.amie.PacketEntity; +import org.apache.custos.access.ci.service.model.amie.ProcessingEventEntity; +import org.apache.custos.access.ci.service.repo.amie.AuditLogRepository; +import org.apache.custos.access.ci.service.repo.amie.PacketRepository; +import org.apache.custos.access.ci.service.repo.amie.ProcessingEventRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@Tag("unit") +class AuditServiceTest { + + @Mock + private AuditLogRepository auditLogRepository; + + @Mock + private PacketRepository packetRepository; + + @Mock + private ProcessingEventRepository processingEventRepository; + + private AuditService auditService; + + @BeforeEach + void setUp() { + auditService = new AuditService(auditLogRepository, packetRepository, processingEventRepository); + } + + @Test + void log_withEventId_shouldCreateAuditEntryWithAllFields() { + String packetId = "packet-001"; + String eventId = "event-abc"; + PacketEntity packetProxy = new PacketEntity(); + ProcessingEventEntity eventProxy = new ProcessingEventEntity(); + + when(packetRepository.getReferenceById(packetId)).thenReturn(packetProxy); + when(processingEventRepository.getReferenceById(eventId)).thenReturn(eventProxy); + when(auditLogRepository.save(any(AuditLogEntity.class))).thenAnswer(inv -> inv.getArgument(0)); + + auditService.log(packetId, eventId, AuditAction.CREATE_PERSON, "Person", "person-123", "Created person from packet"); + + ArgumentCaptor<AuditLogEntity> captor = ArgumentCaptor.forClass(AuditLogEntity.class); + verify(auditLogRepository).save(captor.capture()); + + AuditLogEntity saved = captor.getValue(); + assertThat(saved.getPacket()).isSameAs(packetProxy); + assertThat(saved.getEvent()).isSameAs(eventProxy); + assertThat(saved.getAction()).isEqualTo(AuditAction.CREATE_PERSON); + assertThat(saved.getEntityType()).isEqualTo("Person"); + assertThat(saved.getEntityId()).isEqualTo("person-123"); + assertThat(saved.getSummary()).isEqualTo("Created person from packet"); + assertThat(saved.getCreatedAt()).isNotNull(); + } + + @Test + void log_withNullEventId_shouldCreateAuditEntryWithNullEvent() { + String packetId = "packet-002"; + PacketEntity packetProxy = new PacketEntity(); + + when(packetRepository.getReferenceById(packetId)).thenReturn(packetProxy); + when(auditLogRepository.save(any(AuditLogEntity.class))).thenAnswer(inv -> inv.getArgument(0)); + + auditService.log(packetId, null, AuditAction.REPLY_SENT, "Packet", null, "Reply dispatched"); + + ArgumentCaptor<AuditLogEntity> captor = ArgumentCaptor.forClass(AuditLogEntity.class); + verify(auditLogRepository).save(captor.capture()); + + AuditLogEntity saved = captor.getValue(); + assertThat(saved.getPacket()).isSameAs(packetProxy); + assertThat(saved.getEvent()).isNull(); + assertThat(saved.getAction()).isEqualTo(AuditAction.REPLY_SENT); + assertThat(saved.getEntityType()).isEqualTo("Packet"); + assertThat(saved.getEntityId()).isNull(); + assertThat(saved.getSummary()).isEqualTo("Reply dispatched"); + } + + @Test + void log_withNullSummaryAndEntityId_shouldSaveEntityWithNulls() { + String packetId = "packet-003"; + PacketEntity packetProxy = new PacketEntity(); + + when(packetRepository.getReferenceById(packetId)).thenReturn(packetProxy); + when(auditLogRepository.save(any(AuditLogEntity.class))).thenAnswer(inv -> inv.getArgument(0)); + + auditService.log(packetId, null, AuditAction.TRANSACTION_COMPLETE, "Transaction", null, null); + + ArgumentCaptor<AuditLogEntity> captor = ArgumentCaptor.forClass(AuditLogEntity.class); + verify(auditLogRepository).save(captor.capture()); + + AuditLogEntity saved = captor.getValue(); + assertThat(saved.getAction()).isEqualTo(AuditAction.TRANSACTION_COMPLETE); + assertThat(saved.getEntityId()).isNull(); + assertThat(saved.getSummary()).isNull(); + } + + @Test + void log_usesGetReferenceByIdForPacketProxy() { + String packetId = "packet-004"; + PacketEntity proxy = new PacketEntity(); + when(packetRepository.getReferenceById(packetId)).thenReturn(proxy); + when(auditLogRepository.save(any(AuditLogEntity.class))).thenAnswer(inv -> inv.getArgument(0)); + + auditService.log(packetId, null, AuditAction.CREATE_PROJECT, "Project", "proj-1", null); + + verify(packetRepository).getReferenceById(packetId); + } + + @Test + void log_usesGetReferenceByIdForEventProxy() { + String packetId = "packet-005"; + String eventId = "event-xyz"; + when(packetRepository.getReferenceById(packetId)).thenReturn(new PacketEntity()); + when(processingEventRepository.getReferenceById(eventId)).thenReturn(new ProcessingEventEntity()); + when(auditLogRepository.save(any(AuditLogEntity.class))).thenAnswer(inv -> inv.getArgument(0)); + + auditService.log(packetId, eventId, AuditAction.CREATE_MEMBERSHIP, "Membership", "m-1", null); + + verify(processingEventRepository).getReferenceById(eventId); + } +}
