Security Audit
Audit logging is not “log a string somewhere”. A security audit needs
typed events, a query API, and a sink that survives without external
infrastructure. security-core ships all three.
Konzept-V00.60.00.md.
Delivered 2026-05-10; route + REST endpoint added 2026-05-11.The contract
public interface SecurityAuditService {
void publish(AuditEvent event);
List<AuditEvent> query(AuditQuery query);
}Two implementations come with the library:
NoopSecurityAuditService— accepts and discards. Useful for tests or for production deployments that route events elsewhere.DefaultCompositeAuditService— fanspublish(...)out to one or moreAuditSinks and servesquery(...)from the first sink that supports it.
The 16 event types
AuditEvent is a sealed interface. The full taxonomy:
| Category | Events |
|---|---|
| Authentication | LoginSucceeded, LoginFailed, LogoutPerformed |
| Authorization | AccessGranted, AccessDenied, ActionDenied |
| Role administration | RoleAssigned, RoleRevoked |
| User administration | UserCreated, UserDeleted |
| Sessions | SessionCreated, SessionExpired, SessionInvalidated |
| Bootstrap | BootstrapAdminCreated, BootstrapTokenRejected |
| Brute-force | BruteForceLimitReached |
Every event is an immutable record. Pattern-match on the sealed
hierarchy:
switch (event) {
case LoginSucceeded e -> metrics.loginOk(e.subjectId());
case LoginFailed e -> metrics.loginFail(e.username(), e.reason());
case BruteForceLimitReached e -> alerts.notify(e);
case AccessDenied e -> denyCounter.increment();
// exhaustive — compiler enforces all 16
}Sinks
public interface AuditSink {
void accept(AuditEvent event);
List<AuditEvent> query(AuditQuery query); // optional
}Two come with the library:
LoggingAuditSink— writes every event throughjava.util.loggingat INFO. Zero dependencies, fine for development and for piping into existing log aggregation.RingBufferAuditSink— keeps the last N events in memory. Survives restart only as long as the JVM lives, but answersquery()efficiently and powers the in-app audit viewers below.
Want Postgres / Kafka / S3? Implement AuditSink and register it with
the CompositeAuditService. No core change.
Querying
public record AuditQuery(
Instant since,
Instant until,
Optional<SubjectId> subjectId,
int limit
) {}Used by both viewers; designed to be the same query both in-process and through the REST API so the same code can run against either.
Vaadin /audit route
Both Vaadin demos expose a /audit route, gated by
@RequiresPermission("audit:read"). It renders a Grid backed by
RingBufferAuditSink with filters for subject and time.
REST GET /api/audit
The REST demo exposes the same data via JSON, gated by the same
permission and routed through RestAuthorizationFilter. 403 for
non-admins, 200 + JSON array for admins:
curl -H "Authorization: Bearer <token>" \
http://localhost:8080/api/audit?subject=alice&limit=50Wiring it in your application
public class AppBootstrap {
public static SecurityAuditService createAudit() {
var ring = new RingBufferAuditSink(10_000);
var logging = new LoggingAuditSink();
return new DefaultCompositeAuditService(List.of(ring, logging));
}
}Register via META-INF/services/com.svenruppert.vaadin.security.audit.SecurityAuditService.
The Vaadin and REST adapters publish their built-in events
(LoginSucceeded, AccessDenied, etc.) automatically once the service
is resolved.