diff --git a/CHANGELOG.md b/CHANGELOG.md index c0b05d404..312592bda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - WoT: Admins can adjust WoT parameters (#297) - Permission to create new vaults can now be controlled via the `create-vaults` role in Keycloak (#206) - Preserver user locale setting (#313) -- Add Italian, Korean, Dutch and Portuguese translation +- Italian, Korean, Dutch and Portuguese translation +- Audit log filter by event type ### Changed diff --git a/backend/src/main/java/org/cryptomator/hub/api/AuditLogResource.java b/backend/src/main/java/org/cryptomator/hub/api/AuditLogResource.java index 96d987257..06d542145 100644 --- a/backend/src/main/java/org/cryptomator/hub/api/AuditLogResource.java +++ b/backend/src/main/java/org/cryptomator/hub/api/AuditLogResource.java @@ -38,6 +38,7 @@ import java.time.Instant; import java.util.List; +import java.util.Set; import java.util.UUID; @Path("/auditlog") @@ -57,11 +58,12 @@ public class AuditLogResource { @Parameter(name = "paginationId", description = "The smallest (asc ordering) or highest (desc ordering) audit entry id, not included in results. Used for pagination. ", in = ParameterIn.QUERY) @Parameter(name = "order", description = "The order of the queried table. Determines if most recent (desc) or oldest entries (asc) are considered first. Allowed Values are 'desc' (default) or 'asc'. Used for pagination.", in = ParameterIn.QUERY) @Parameter(name = "pageSize", description = "the maximum number of entries to return. Must be between 1 and 100.", in = ParameterIn.QUERY) + @Parameter(name = "type", description = "the list of type of events to return. Empty list is all events.", in = ParameterIn.QUERY) @APIResponse(responseCode = "200", description = "Body contains list of events in the specified time interval") - @APIResponse(responseCode = "400", description = "startDate or endDate not specified, startDate > endDate, order specified and not in ['asc','desc'] or pageSize not in [1 .. 100]") + @APIResponse(responseCode = "400", description = "startDate or endDate not specified, startDate > endDate, order specified and not in ['asc','desc'], pageSize not in [1 .. 100] or type is not valid") @APIResponse(responseCode = "402", description = "Community license used or license expired") @APIResponse(responseCode = "403", description = "requesting user does not have admin role") - public List getAllEvents(@QueryParam("startDate") Instant startDate, @QueryParam("endDate") Instant endDate, @QueryParam("paginationId") Long paginationId, @QueryParam("order") @DefaultValue("desc") String order, @QueryParam("pageSize") @DefaultValue("20") int pageSize) { + public List getAllEvents(@QueryParam("startDate") Instant startDate, @QueryParam("endDate") Instant endDate, @QueryParam("type") List type, @QueryParam("paginationId") Long paginationId, @QueryParam("order") @DefaultValue("desc") String order, @QueryParam("pageSize") @DefaultValue("20") int pageSize) { if (!license.isSet() || license.isExpired()) { throw new PaymentRequiredException("Community license used or license expired"); } @@ -74,11 +76,20 @@ public List getAllEvents(@QueryParam("startDate") Instant startDa throw new BadRequestException("order must be either 'asc' or 'desc'"); } else if (pageSize < 1 || pageSize > 100) { throw new BadRequestException("pageSize must be between 1 and 100"); + } else if (type == null) { + throw new BadRequestException("type must be specified"); + } else if (!type.isEmpty()) { + var validTypes = Set.of(DeviceRegisteredEvent.TYPE, DeviceRemovedEvent.TYPE, UserAccountResetEvent.TYPE, UserKeysChangeEvent.TYPE, UserSetupCodeChangeEvent.TYPE, + SettingWotUpdateEvent.TYPE, SignedWotIdEvent.TYPE, VaultCreatedEvent.TYPE, VaultUpdatedEvent.TYPE, VaultAccessGrantedEvent.TYPE, + VaultKeyRetrievedEvent.TYPE, VaultMemberAddedEvent.TYPE, VaultMemberRemovedEvent.TYPE, VaultMemberUpdatedEvent.TYPE, VaultOwnershipClaimedEvent.TYPE); + if (!validTypes.containsAll(type)) { + throw new BadRequestException("Invalid event type provided"); + } } else if (paginationId == null) { throw new BadRequestException("paginationId must be specified"); } - return auditEventRepo.findAllInPeriod(startDate, endDate, paginationId, order.equals("asc"), pageSize).map(AuditEventDto::fromEntity).toList(); + return auditEventRepo.findAllInPeriod(startDate, endDate, type, paginationId, order.equals("asc"), pageSize).map(AuditEventDto::fromEntity).toList(); } @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") diff --git a/backend/src/main/java/org/cryptomator/hub/entities/events/AuditEvent.java b/backend/src/main/java/org/cryptomator/hub/entities/events/AuditEvent.java index e19e16302..793592ead 100644 --- a/backend/src/main/java/org/cryptomator/hub/entities/events/AuditEvent.java +++ b/backend/src/main/java/org/cryptomator/hub/entities/events/AuditEvent.java @@ -17,6 +17,7 @@ import jakarta.persistence.Table; import java.time.Instant; +import java.util.List; import java.util.Objects; import java.util.stream.Stream; @@ -31,6 +32,7 @@ WHERE ae.timestamp >= :startDate AND ae.timestamp < :endDate AND ae.id < :paginationId + AND (:allTypes = true OR ae.type IN :types) ORDER BY ae.id DESC """) @NamedQuery(name = "AuditEvent.listAllInPeriodAfterId", @@ -40,6 +42,7 @@ WHERE ae.timestamp >= :startDate AND ae.timestamp < :endDate AND ae.id > :paginationId + AND (:allTypes = true OR ae.type IN :types) ORDER BY ae.id ASC """) @SequenceGenerator(name = "audit_event_id_seq", sequenceName = "audit_event_id_seq", allocationSize = 1) @@ -106,8 +109,14 @@ public int hashCode() { @ApplicationScoped public static class Repository implements PanacheRepository { - public Stream findAllInPeriod(Instant startDate, Instant endDate, long paginationId, boolean ascending, int pageSize) { - var parameters = Parameters.with("startDate", startDate).and("endDate", endDate).and("paginationId", paginationId); + public Stream findAllInPeriod(Instant startDate, Instant endDate, List type, long paginationId, boolean ascending, int pageSize) { + var allTypes = type.isEmpty(); + + var parameters = Parameters.with("startDate", startDate) + .and("endDate", endDate) + .and("paginationId", paginationId) + .and("types", type) + .and("allTypes", allTypes); final PanacheQuery query; if (ascending) { diff --git a/frontend/src/common/auditlog.ts b/frontend/src/common/auditlog.ts index bd72fc65b..d32b78f11 100644 --- a/frontend/src/common/auditlog.ts +++ b/frontend/src/common/auditlog.ts @@ -178,8 +178,9 @@ export class AuditLogEntityCache { /* Service */ class AuditLogService { - public async getAllEvents(startDate: Date, endDate: Date, paginationId: number, order: string, pageSize: number): Promise { - return axiosAuth.get(`/auditlog?startDate=${startDate.toISOString()}&endDate=${endDate.toISOString()}&paginationId=${paginationId}&order=${order}&pageSize=${pageSize}`) + public async getAllEvents(startDate: Date, endDate: Date, type: string[], paginationId: number, order: string, pageSize: number): Promise { + const typeQuery = type.length > 0 ? `&type=${type.join('&type=')}` : ''; + return axiosAuth.get(`/auditlog?startDate=${startDate.toISOString()}&endDate=${endDate.toISOString()}&paginationId=${paginationId}${typeQuery}&order=${order}&pageSize=${pageSize}`) .then(response => response.data.map(dto => { dto.timestamp = new Date(dto.timestamp); return dto; diff --git a/frontend/src/components/AuditLog.vue b/frontend/src/components/AuditLog.vue index af6ed4874..68b70fc8c 100644 --- a/frontend/src/components/AuditLog.vue +++ b/frontend/src/components/AuditLog.vue @@ -41,14 +41,14 @@ - + {{ t('auditLog.filter') }} - +
+
+ +
+ +
+ +
+ + +
+ + +
+ + + + {{ label }} + + + + + +
+
-
@@ -166,7 +222,7 @@