diff --git a/src/main/java/net/staticstudios/audit/AuditLogEntry.java b/src/main/java/net/staticstudios/audit/AuditLogEntry.java index 8d90099..0556f36 100644 --- a/src/main/java/net/staticstudios/audit/AuditLogEntry.java +++ b/src/main/java/net/staticstudios/audit/AuditLogEntry.java @@ -11,7 +11,7 @@ * Represents an entry in the audit log. */ public class AuditLogEntry { - private final @NotNull UUID userId; + private final @NotNull AuditUser user; private final @Nullable UUID sessionId; private final @NotNull String applicationGroup; private final @NotNull String applicationId; @@ -22,7 +22,7 @@ public class AuditLogEntry { /** * Creates a new AuditLogEntry. * - * @param userId the ID of the user who performed the action + * @param user the ID of the user who performed the action * @param sessionId the ID of the session in which the action was performed (nullable) * @param applicationGroup the application group that logged the action * @param applicationId the application ID that logged the action @@ -30,8 +30,8 @@ public class AuditLogEntry { * @param action the action that was performed * @param data the data associated with the action */ - public AuditLogEntry(@NotNull UUID userId, @Nullable UUID sessionId, @NotNull String applicationGroup, @NotNull String applicationId, @NotNull Instant timestamp, @NotNull Action action, @NotNull T data) { - this.userId = userId; + public AuditLogEntry(@NotNull AuditUser user, @Nullable UUID sessionId, @NotNull String applicationGroup, @NotNull String applicationId, @NotNull Instant timestamp, @NotNull Action action, @NotNull T data) { + this.user = user; this.sessionId = sessionId; this.applicationGroup = applicationGroup; this.applicationId = applicationId; @@ -45,8 +45,8 @@ public AuditLogEntry(@NotNull UUID userId, @Nullable UUID sessionId, @NotNull St * * @return the user ID */ - public @NotNull UUID getUserId() { - return userId; + public @NotNull AuditUser getUser() { + return user; } /** @@ -111,7 +111,7 @@ public AuditLogEntry(@NotNull UUID userId, @Nullable UUID sessionId, @NotNull St @Override public String toString() { return String.format("AuditLogEntry{userId=%s, sessionId=%s, applicationGroup='%s', applicationId='%s', timestamp=%s, action=%s, data=%s}", - userId, sessionId, applicationGroup, applicationId, timestamp, action, data); + user, sessionId, applicationGroup, applicationId, timestamp, action, data); } /** @@ -126,7 +126,7 @@ public boolean equals(Object obj) { if (obj == null || getClass() != obj.getClass()) return false; AuditLogEntry that = (AuditLogEntry) obj; return Objects.equals(action, that.action) && Objects.equals(data, that.data) && - Objects.equals(userId, that.userId) && Objects.equals(sessionId, that.sessionId) && + Objects.equals(user, that.user) && Objects.equals(sessionId, that.sessionId) && Objects.equals(applicationGroup, that.applicationGroup) && Objects.equals(applicationId, that.applicationId) && Objects.equals(timestamp, that.timestamp); } @@ -138,6 +138,6 @@ public boolean equals(Object obj) { */ @Override public int hashCode() { - return Objects.hash(action, data, userId, sessionId, applicationGroup, applicationId, timestamp); + return Objects.hash(action, data, user, sessionId, applicationGroup, applicationId, timestamp); } } diff --git a/src/main/java/net/staticstudios/audit/AuditUser.java b/src/main/java/net/staticstudios/audit/AuditUser.java new file mode 100644 index 0000000..c355fc4 --- /dev/null +++ b/src/main/java/net/staticstudios/audit/AuditUser.java @@ -0,0 +1,36 @@ +package net.staticstudios.audit; + +import java.util.Objects; +import java.util.UUID; + +public class AuditUser { + + private final String id; + + private AuditUser(String id) { + this.id = id; + } + + public static AuditUser of(String id) { + return new AuditUser(id); + } + + public static AuditUser of(UUID uuid) { + return new AuditUser(uuid.toString()); + } + + public String getId() { + return id; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof AuditUser auditUser)) return false; + return Objects.equals(id, auditUser.id); + } + + @Override + public int hashCode() { + return Objects.hashCode(id); + } +} diff --git a/src/main/java/net/staticstudios/audit/EncodedAuditLogEntry.java b/src/main/java/net/staticstudios/audit/EncodedAuditLogEntry.java index 425738e..68f4e29 100644 --- a/src/main/java/net/staticstudios/audit/EncodedAuditLogEntry.java +++ b/src/main/java/net/staticstudios/audit/EncodedAuditLogEntry.java @@ -7,7 +7,7 @@ import java.util.UUID; public class EncodedAuditLogEntry { - private final @NotNull UUID userId; + private final @NotNull AuditUser user; private final @Nullable UUID sessionId; private final @NotNull String applicationGroup; private final @NotNull String applicationId; @@ -15,8 +15,8 @@ public class EncodedAuditLogEntry { private final @NotNull String actionId; private final @NotNull String encodedData; - public EncodedAuditLogEntry(@NotNull UUID userId, @Nullable UUID sessionId, @NotNull String applicationGroup, @NotNull String applicationId, @NotNull Instant timestamp, @NotNull String actionId, @NotNull String encodedData) { - this.userId = userId; + public EncodedAuditLogEntry(@NotNull AuditUser user, @Nullable UUID sessionId, @NotNull String applicationGroup, @NotNull String applicationId, @NotNull Instant timestamp, @NotNull String actionId, @NotNull String encodedData) { + this.user = user; this.sessionId = sessionId; this.applicationGroup = applicationGroup; this.applicationId = applicationId; @@ -25,8 +25,8 @@ public EncodedAuditLogEntry(@NotNull UUID userId, @Nullable UUID sessionId, @Not this.encodedData = encodedData; } - public @NotNull UUID getUserId() { - return userId; + public @NotNull AuditUser getUser() { + return user; } public @Nullable UUID getSessionId() { diff --git a/src/main/java/net/staticstudios/audit/StaticAudit.java b/src/main/java/net/staticstudios/audit/StaticAudit.java index 214c982..bb4f6eb 100644 --- a/src/main/java/net/staticstudios/audit/StaticAudit.java +++ b/src/main/java/net/staticstudios/audit/StaticAudit.java @@ -34,7 +34,7 @@ public class StaticAudit { session_id UUID, application_group VARCHAR(255) NOT NULL, application_id VARCHAR(255) NOT NULL, - user_id UUID NOT NULL, + user_id VARCHAR(255) NOT NULL, action_id VARCHAR(255) NOT NULL, action_data JSONB ); @@ -60,7 +60,9 @@ private StaticAudit(String applicationGroup, String applicationId, String schema run(connection -> { logger.info("Creating audit log table... ({}.{})", schemaName, tableName); - String sql = String.format(TABLE_DEF, schemaName, tableName); + String sql = String.format(TABLE_DEF, + schemaName, tableName //CREATE + ); try (Statement statement = connection.createStatement()) { statement.execute(sql); logger.trace(sql); @@ -159,55 +161,56 @@ public StaticAudit registerAction(Action action) { /** * Logs an action to the audit log. * - * @param userId The ID of the user performing the action. + * @param user The ID of the user performing the action. * @param sessionId The user's current session ID, if applicable. * @param timestamp The timestamp of when the action occurred. * @param action The action being performed. * @param actionData The data associated with the action. * @return The StaticAudit instance. */ - public StaticAudit log(@NotNull UUID userId, @Nullable UUID sessionId, @NotNull Instant timestamp, @NotNull Action action, @NotNull T actionData) { - return log(userId, sessionId, timestamp, action.getActionId(), actionData); + public StaticAudit log(@NotNull AuditUser user, @Nullable UUID sessionId, @NotNull Instant timestamp, @NotNull Action action, @NotNull T actionData) { + return log(user, sessionId, timestamp, action.getActionId(), actionData); } /** * Logs an action to the audit log. * - * @param userId The ID of the user performing the action. + * @param user The ID of the user performing the action. * @param sessionId The user's current session ID, if applicable. * @param action The action being performed. * @param actionData The data associated with the action. * @return The StaticAudit instance. */ - public StaticAudit log(@NotNull UUID userId, @Nullable UUID sessionId, @NotNull Action action, @NotNull T actionData) { - return log(userId, sessionId, Instant.now(), action.getActionId(), actionData); + public StaticAudit log(@NotNull AuditUser user, @Nullable UUID sessionId, @NotNull Action action, @NotNull T actionData) { + return log(user, sessionId, Instant.now(), action.getActionId(), actionData); } /** * Logs an action to the audit log. * - * @param userId The ID of the user performing the action. + * @param user The ID of the user performing the action. * @param sessionId The user's current session ID, if applicable. * @param actionId The ID of the action being performed. * @param actionData The data associated with the action. * @return The StaticAudit instance. */ - public StaticAudit log(@NotNull UUID userId, @Nullable UUID sessionId, @NotNull String actionId, @NotNull Object actionData) { - return log(userId, sessionId, Instant.now(), actionId, actionData); + public StaticAudit log(@NotNull AuditUser user, @Nullable UUID sessionId, @NotNull String actionId, @NotNull Object actionData) { + return log(user, sessionId, Instant.now(), actionId, actionData); } + /** * Logs an action to the audit log. * - * @param userId The ID of the user performing the action. + * @param user The user performing the action. * @param sessionId The user's current session ID, if applicable. * @param timestamp The timestamp of when the action occurred. * @param actionId The ID of the action being performed. * @param actionData The data associated with the action. * @return The StaticAudit instance. */ - public StaticAudit log(@NotNull UUID userId, @Nullable UUID sessionId, @NotNull Instant timestamp, @NotNull String actionId, @NotNull Object actionData) { - Preconditions.checkNotNull(userId, "User ID cannot be null"); + public StaticAudit log(@NotNull AuditUser user, @Nullable UUID sessionId, @NotNull Instant timestamp, @NotNull String actionId, @NotNull Object actionData) { + Preconditions.checkNotNull(user, "User cannot be null"); Preconditions.checkNotNull(timestamp, "Timestamp cannot be null"); Preconditions.checkNotNull(actionId, "Action ID cannot be null"); Preconditions.checkNotNull(actionData, "Action data cannot be null"); @@ -224,7 +227,7 @@ public StaticAudit log(@NotNull UUID userId, @Nullable UUID sessionId, @NotNull statement.setObject(3, sessionId); statement.setString(4, applicationGroup); statement.setString(5, applicationId); - statement.setObject(6, userId); + statement.setObject(6, user.getId()); statement.setString(7, actionId); statement.setString(8, actionDataJson); logger.trace(statement.toString()); @@ -238,7 +241,7 @@ public StaticAudit log(@NotNull UUID userId, @Nullable UUID sessionId, @NotNull /** * Retrieves audit log entries asynchronously for a given user and optional filters. * - * @param userId The ID of the user whose logs to retrieve. + * @param user The ID of the user whose logs to retrieve. * @param sessionId The session ID to filter by, or null for any session. * @param from The start timestamp for filtering, or null for no lower bound. * @param to The end timestamp for filtering, or null for no upper bound. @@ -246,16 +249,16 @@ public StaticAudit log(@NotNull UUID userId, @Nullable UUID sessionId, @NotNull * @param limit The maximum number of entries to retrieve. * @return A CompletableFuture containing the list of matching audit log entries. */ - public CompletableFuture>> retrieveAsync(@NotNull UUID userId, @Nullable UUID sessionId, @Nullable Instant from, @Nullable Instant to, int limit, String... actionIds) { + public CompletableFuture>> retrieveAsync(@NotNull AuditUser user, @Nullable UUID sessionId, @Nullable Instant from, @Nullable Instant to, int limit, String... actionIds) { CompletableFuture>> future = new CompletableFuture<>(); - async(() -> future.complete(retrieve(userId, sessionId, from, to, limit, actionIds))); + async(() -> future.complete(retrieve(user, sessionId, from, to, limit, actionIds))); return future; } /** * Retrieves audit log entries for a given user and optional filters. * - * @param userId The ID of the user whose logs to retrieve. + * @param user The ID of the user whose logs to retrieve. * @param sessionId The session ID to filter by, or null for any session. * @param from The start timestamp for filtering, or null for no lower bound. * @param to The end timestamp for filtering, or null for no upper bound. @@ -263,8 +266,8 @@ public CompletableFuture>> retrieveAsync(@NotNull UUID use * @param limit The maximum number of entries to retrieve. * @return The list of matching audit log entries. */ - public List> retrieve(@NotNull UUID userId, @Nullable UUID sessionId, @Nullable Instant from, @Nullable Instant to, int limit, String... actionIds) { - List encodedList = retrieveEncoded(userId, sessionId, from, to, limit, actionIds); + public List> retrieve(@NotNull AuditUser user, @Nullable UUID sessionId, @Nullable Instant from, @Nullable Instant to, int limit, String... actionIds) { + List encodedList = retrieveEncoded(user, sessionId, from, to, limit, actionIds); encodedList.removeIf(encoded -> { if (!actions.containsKey(encoded.getActionId())) { logger.warn("Unknown action ID {} in audit log, skipping entry", encoded.getActionId()); @@ -285,7 +288,7 @@ public List> retrieve(@NotNull UUID userId, @Nullable UUID sess /** * Retrieves encoded audit log entries asynchronously for a given user and optional filters. * - * @param userId The ID of the user whose logs to retrieve. + * @param user The ID of the user whose logs to retrieve. * @param sessionId The session ID to filter by, or null for any session. * @param from The start timestamp for filtering, or null for no lower bound. * @param to The end timestamp for filtering, or null for no upper bound. @@ -293,16 +296,16 @@ public List> retrieve(@NotNull UUID userId, @Nullable UUID sess * @param limit The maximum number of entries to retrieve. * @return A CompletableFuture containing the list of matching audit log entries. */ - public CompletableFuture> retrieveEncodedAsync(@NotNull UUID userId, @Nullable UUID sessionId, @Nullable Instant from, @Nullable Instant to, int limit, String... actionIds) { + public CompletableFuture> retrieveEncodedAsync(@NotNull AuditUser user, @Nullable UUID sessionId, @Nullable Instant from, @Nullable Instant to, int limit, String... actionIds) { CompletableFuture> future = new CompletableFuture<>(); - async(() -> future.complete(retrieveEncoded(userId, sessionId, from, to, limit, actionIds))); + async(() -> future.complete(retrieveEncoded(user, sessionId, from, to, limit, actionIds))); return future; } /** * Retrieves encoded audit log entries for a given user and optional filters. * - * @param userId The ID of the user whose logs to retrieve. + * @param user The ID of the user whose logs to retrieve. * @param sessionId The session ID to filter by, or null for any session. * @param from The start timestamp for filtering, or null for no lower bound. * @param to The end timestamp for filtering, or null for no upper bound. @@ -310,8 +313,8 @@ public CompletableFuture> retrieveEncodedAsync(@NotNu * @param limit The maximum number of entries to retrieve. * @return The list of matching audit log entries. */ - public List retrieveEncoded(@NotNull UUID userId, @Nullable UUID sessionId, @Nullable Instant from, @Nullable Instant to, int limit, String... actionIds) { - Preconditions.checkNotNull(userId, "User ID cannot be null"); + public List retrieveEncoded(@NotNull AuditUser user, @Nullable UUID sessionId, @Nullable Instant from, @Nullable Instant to, int limit, String... actionIds) { + Preconditions.checkNotNull(user, "User ID cannot be null"); Preconditions.checkArgument(limit > 0, "Limit must be greater than 0"); List entries = new ArrayList<>(); @@ -340,7 +343,7 @@ public List retrieveEncoded(@NotNull UUID userId, @Nullabl String sql = sqlBuilder.toString().formatted(schemaName, tableName); try (PreparedStatement statement = connection.prepareStatement(sql)) { int index = 1; - statement.setObject(index++, userId); + statement.setObject(index++, user.getId()); if (sessionId != null) { statement.setObject(index++, sessionId); } @@ -359,7 +362,7 @@ public List retrieveEncoded(@NotNull UUID userId, @Nullabl while (rs.next()) { String _actionId = rs.getString("action_id"); EncodedAuditLogEntry entry = new EncodedAuditLogEntry( - (UUID) rs.getObject("user_id"), + AuditUser.of(rs.getString("user_id")), (UUID) rs.getObject("session_id"), rs.getString("application_group"), rs.getString("application_id"), @@ -385,7 +388,7 @@ private T fromJson(Action action, String json) { private AuditLogEntry createEntry(EncodedAuditLogEntry encoded) { return createEntry( - encoded.getUserId(), + encoded.getUser(), encoded.getSessionId(), encoded.getApplicationGroup(), encoded.getApplicationId(), @@ -396,7 +399,7 @@ private AuditLogEntry createEntry(EncodedAuditLogEntry encoded) { } private AuditLogEntry createEntry( - UUID userId, UUID sessionId, String applicationGroup, String applicationId, Instant timestamp, + AuditUser userId, UUID sessionId, String applicationGroup, String applicationId, Instant timestamp, Action action, String jsonData) { return new AuditLogEntry<>(userId, sessionId, applicationGroup, applicationId, timestamp, action, fromJson(action, jsonData)); diff --git a/src/test/java/net/staticstudios/audit/LoggingTest.java b/src/test/java/net/staticstudios/audit/LoggingTest.java index ff4e739..7bf671c 100644 --- a/src/test/java/net/staticstudios/audit/LoggingTest.java +++ b/src/test/java/net/staticstudios/audit/LoggingTest.java @@ -15,7 +15,7 @@ public class LoggingTest extends AuditTest { private static final Instant NOW = Instant.ofEpochMilli(0); private StaticAudit audit; - private UUID userId; + private AuditUser auditUser; private UUID sessionId; private Action action1; private Action action2; @@ -31,7 +31,7 @@ public void setUp() { .closeConnections(false) .build(); - userId = UUID.randomUUID(); + auditUser = AuditUser.of(UUID.randomUUID()); sessionId = UUID.randomUUID(); action1 = Action.simple("test_action", SimpleActionData.class); action2 = Action.simple("test_action_2", SimpleActionData.class); @@ -51,15 +51,15 @@ public void tearDown() throws SQLException { @Test public void testLogging() throws SQLException { SimpleActionData data = new SimpleActionData("test"); - audit.log(userId, sessionId, action1, data); + audit.log(auditUser, sessionId, action1, data); Connection connection = getConnection(); @Language("SQL") String sql = "SELECT * FROM %s.%s WHERE user_id = ?"; PreparedStatement statement = connection.prepareStatement(sql.formatted(audit.getSchemaName(), audit.getTableName())); - statement.setObject(1, userId); + statement.setObject(1, auditUser.getId()); ResultSet rs = statement.executeQuery(); assertTrue(rs.next()); - assertEquals(userId, rs.getObject("user_id")); + assertEquals(auditUser.getId(), rs.getObject("user_id")); assertEquals(sessionId, rs.getObject("session_id")); assertEquals(action1.getActionId(), rs.getString("action_id")); assertEquals(data, action1.fromJson(rs.getString("action_data"))); @@ -69,10 +69,11 @@ public void testLogging() throws SQLException { public void testRetrieving() { logMultiple(50); + List> entries; - entries = audit.retrieve(userId, null, null, null, 100); + entries = audit.retrieve(auditUser, null, null, null, 100); assertEquals(50, entries.size()); - entries = audit.retrieve(userId, null, null, null, 10); + entries = audit.retrieve(auditUser, null, null, null, 10); assertEquals(10, entries.size()); for (int i = 0; i < 10; i++) { @@ -88,12 +89,12 @@ public void testRetrievingWithFilter() { List> entries; - entries = audit.retrieve(userId, null, null, null, 500); + entries = audit.retrieve(auditUser, null, null, null, 500); assertEquals(140, entries.size()); - entries = audit.retrieve(userId, null, null, null, 100, action1.getActionId(), action3.getActionId()); + entries = audit.retrieve(auditUser, null, null, null, 100, action1.getActionId(), action3.getActionId()); assertEquals(100, entries.size()); assertFalse(entries.stream().anyMatch(entry -> entry.getAction().getActionId().equals(action2.getActionId()))); - entries = audit.retrieve(userId, null, null, null, 10, action1.getActionId()); + entries = audit.retrieve(auditUser, null, null, null, 10, action1.getActionId()); assertEquals(10, entries.size()); assertTrue(entries.stream().allMatch(entry -> entry.getAction().getActionId().equals(action1.getActionId()))); assertFalse(entries.stream().anyMatch(entry -> entry.getAction().getActionId().equals(action2.getActionId()))); @@ -104,7 +105,7 @@ public void testRetrievingWithFilter() { public void testRetrievingEncoded() { logMultiple(action1, 5); logMultiple(action2, 3); - List encodedEntries = audit.retrieveEncoded(userId, null, null, null, 20); + List encodedEntries = audit.retrieveEncoded(auditUser, null, null, null, 20); assertEquals(8, encodedEntries.size()); assertTrue(encodedEntries.stream().anyMatch(e -> e.getActionId().equals(action1.getActionId()))); assertTrue(encodedEntries.stream().anyMatch(e -> e.getActionId().equals(action2.getActionId()))); @@ -123,12 +124,12 @@ public void testRetrievingEncodedWithUnknownAction() throws SQLException { statement.setObject(3, sessionId); statement.setString(4, audit.getApplicationGroup()); statement.setString(5, audit.getApplicationId()); - statement.setObject(6, userId); + statement.setObject(6, auditUser.getId()); statement.setString(7, unknownActionId); statement.setString(8, jsonData); statement.executeUpdate(); } - List encodedEntries = audit.retrieveEncoded(userId, null, null, null, 10); + List encodedEntries = audit.retrieveEncoded(auditUser, null, null, null, 10); assertTrue(encodedEntries.stream().anyMatch(e -> e.getActionId().equals(unknownActionId))); } @@ -145,12 +146,12 @@ public void testRetrievingWithUnknownActionIsFiltered() throws SQLException { statement.setObject(3, sessionId); statement.setString(4, audit.getApplicationGroup()); statement.setString(5, audit.getApplicationId()); - statement.setObject(6, userId); + statement.setObject(6, auditUser.getId()); statement.setString(7, unknownActionId); statement.setString(8, jsonData); statement.executeUpdate(); } - List> entries = audit.retrieve(userId, null, null, null, 10); + List> entries = audit.retrieve(auditUser, null, null, null, 10); assertTrue(entries.stream().noneMatch(e -> e.getAction().getActionId().equals(unknownActionId))); } @@ -162,7 +163,7 @@ private void logMultiple(Action action, int count) { for (int i = 0; i < count; i++) { SimpleActionData data = new SimpleActionData("test" + i); Instant timestamp = NOW.plusSeconds(i); - audit.log(userId, sessionId, timestamp, action, data); + audit.log(auditUser, sessionId, timestamp, action, data); } } }