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);
+    }
+}

Reply via email to