fix: resolve actor name from Logto for audit log entries
AuditService now looks up username/name/email from Logto Management API when actorEmail is null, with an in-memory cache to avoid repeated calls. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,8 @@
|
|||||||
package net.siegeln.cameleer.saas.audit;
|
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.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -7,20 +10,29 @@ import org.springframework.stereotype.Service;
|
|||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class AuditService {
|
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<String, String> userNameCache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public AuditService(AuditRepository auditRepository, LogtoManagementClient logtoClient) {
|
||||||
this.auditRepository = auditRepository;
|
this.auditRepository = auditRepository;
|
||||||
|
this.logtoClient = logtoClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void log(UUID actorId, String actorEmail, UUID tenantId,
|
public void log(UUID actorId, String actorEmail, UUID tenantId,
|
||||||
AuditAction action, String resource,
|
AuditAction action, String resource,
|
||||||
String environment, String sourceIp,
|
String environment, String sourceIp,
|
||||||
String result, Map<String, Object> metadata) {
|
String result, Map<String, Object> metadata) {
|
||||||
|
if (actorEmail == null && actorId != null) {
|
||||||
|
actorEmail = resolveActorName(actorId.toString());
|
||||||
|
}
|
||||||
var entry = new AuditEntity();
|
var entry = new AuditEntity();
|
||||||
entry.setActorId(actorId);
|
entry.setActorId(actorId);
|
||||||
entry.setActorEmail(actorEmail);
|
entry.setActorEmail(actorEmail);
|
||||||
@@ -39,4 +51,23 @@ public class AuditService {
|
|||||||
Pageable pageable) {
|
Pageable pageable) {
|
||||||
return auditRepository.findFiltered(tenantId, action, result, from, to, search, 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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -398,6 +398,22 @@ public class LogtoManagementClient {
|
|||||||
.toBodilessEntity();
|
.toBodilessEntity();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Get a user by ID. Returns username, primaryEmail, name. */
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public Map<String, Object> getUser(String userId) {
|
||||||
|
if (!isAvailable() || userId == null) return null;
|
||||||
|
try {
|
||||||
|
return (Map<String, Object>) 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 static final String MGMT_API_RESOURCE = "https://default.logto.app/api";
|
||||||
|
|
||||||
private synchronized String getAccessToken() {
|
private synchronized String getAccessToken() {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package net.siegeln.cameleer.saas.audit;
|
package net.siegeln.cameleer.saas.audit;
|
||||||
|
|
||||||
|
import net.siegeln.cameleer.saas.identity.LogtoManagementClient;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
@@ -20,11 +21,14 @@ class AuditServiceTest {
|
|||||||
@Mock
|
@Mock
|
||||||
private AuditRepository auditRepository;
|
private AuditRepository auditRepository;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private LogtoManagementClient logtoClient;
|
||||||
|
|
||||||
private AuditService auditService;
|
private AuditService auditService;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
auditService = new AuditService(auditRepository);
|
auditService = new AuditService(auditRepository, logtoClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user