diff --git a/src/main/java/net/siegeln/cameleer/saas/audit/AuditService.java b/src/main/java/net/siegeln/cameleer/saas/audit/AuditService.java index d5cf1e7..68ac97b 100644 --- a/src/main/java/net/siegeln/cameleer/saas/audit/AuditService.java +++ b/src/main/java/net/siegeln/cameleer/saas/audit/AuditService.java @@ -1,5 +1,8 @@ package net.siegeln.cameleer.saas.audit; +import net.siegeln.cameleer.saas.identity.LogtoManagementClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -7,20 +10,29 @@ import org.springframework.stereotype.Service; import java.time.Instant; import java.util.Map; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; @Service public class AuditService { - private final AuditRepository auditRepository; + private static final Logger log = LoggerFactory.getLogger(AuditService.class); - public AuditService(AuditRepository auditRepository) { + private final AuditRepository auditRepository; + private final LogtoManagementClient logtoClient; + private final ConcurrentHashMap userNameCache = new ConcurrentHashMap<>(); + + public AuditService(AuditRepository auditRepository, LogtoManagementClient logtoClient) { this.auditRepository = auditRepository; + this.logtoClient = logtoClient; } public void log(UUID actorId, String actorEmail, UUID tenantId, AuditAction action, String resource, String environment, String sourceIp, String result, Map metadata) { + if (actorEmail == null && actorId != null) { + actorEmail = resolveActorName(actorId.toString()); + } var entry = new AuditEntity(); entry.setActorId(actorId); entry.setActorEmail(actorEmail); @@ -39,4 +51,23 @@ public class AuditService { Pageable pageable) { return auditRepository.findFiltered(tenantId, action, result, from, to, search, pageable); } + + private String resolveActorName(String userId) { + return userNameCache.computeIfAbsent(userId, id -> { + try { + var user = logtoClient.getUser(id); + if (user == null) return id; + var username = user.get("username"); + if (username != null && !username.toString().isBlank()) return username.toString(); + var name = user.get("name"); + if (name != null && !name.toString().isBlank()) return name.toString(); + var email = user.get("primaryEmail"); + if (email != null && !email.toString().isBlank()) return email.toString(); + return id; + } catch (Exception e) { + log.warn("Failed to resolve actor name for {}: {}", id, e.getMessage()); + return id; + } + }); + } } diff --git a/src/main/java/net/siegeln/cameleer/saas/identity/LogtoManagementClient.java b/src/main/java/net/siegeln/cameleer/saas/identity/LogtoManagementClient.java index 7b2129b..77dea8b 100644 --- a/src/main/java/net/siegeln/cameleer/saas/identity/LogtoManagementClient.java +++ b/src/main/java/net/siegeln/cameleer/saas/identity/LogtoManagementClient.java @@ -398,6 +398,22 @@ public class LogtoManagementClient { .toBodilessEntity(); } + /** Get a user by ID. Returns username, primaryEmail, name. */ + @SuppressWarnings("unchecked") + public Map getUser(String userId) { + if (!isAvailable() || userId == null) return null; + try { + return (Map) restClient.get() + .uri(config.getLogtoEndpoint() + "/api/users/" + userId) + .header("Authorization", "Bearer " + getAccessToken()) + .retrieve() + .body(Map.class); + } catch (Exception e) { + log.warn("Failed to get user {}: {}", userId, e.getMessage()); + return null; + } + } + private static final String MGMT_API_RESOURCE = "https://default.logto.app/api"; private synchronized String getAccessToken() { diff --git a/src/test/java/net/siegeln/cameleer/saas/audit/AuditServiceTest.java b/src/test/java/net/siegeln/cameleer/saas/audit/AuditServiceTest.java index 1f67568..651edb1 100644 --- a/src/test/java/net/siegeln/cameleer/saas/audit/AuditServiceTest.java +++ b/src/test/java/net/siegeln/cameleer/saas/audit/AuditServiceTest.java @@ -1,5 +1,6 @@ package net.siegeln.cameleer.saas.audit; +import net.siegeln.cameleer.saas.identity.LogtoManagementClient; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -20,11 +21,14 @@ class AuditServiceTest { @Mock private AuditRepository auditRepository; + @Mock + private LogtoManagementClient logtoClient; + private AuditService auditService; @BeforeEach void setUp() { - auditService = new AuditService(auditRepository); + auditService = new AuditService(auditRepository, logtoClient); } @Test