Auditing
What is audited
Write operations in multiple services are annotated with @Auditable (package com.friendly.ftconfigsservice.audit).
Examples:
-
AuthService.completeFirstLogin(UPDATE) -
TabService.importItems(IMPORT) -
NorthboundConfigService.updateConfiguration(UPDATE) -
UserAdminService.updateUserRole(ROLE_CHANGE)
The annotation drives publication of an AuditEvent that is handled after commit.
Supported operations
The AuditOperation enum defines:
| Operation | Severity | Destructive | Description |
|---|---|---|---|
|
LOW |
No |
Entity created |
|
LOW |
No |
Entity modified |
|
HIGH |
Yes |
Entity removed |
|
MEDIUM |
No |
Bulk import from file |
|
LOW |
No |
Sensitive read recorded |
|
LOW |
No |
Uncategorized action |
|
MEDIUM |
No |
User password changed |
|
HIGH |
Yes |
Session revoked |
|
HIGH |
No |
User role changed |
|
HIGH |
Yes |
User account suspended |
|
MEDIUM |
No |
User account activated |
Each operation carries severity and destructive metadata used by the frontend to render badges, colour-code actions, and filter destructive operations.
Operation metadata endpoint
GET /api/admin/audit-log/operations returns List<AuditOperationDto>:
[
{
"code": "DELETE",
"label": "Delete",
"severity": "HIGH",
"destructive": true
}
]
Labels are localized to the current user’s locale via audit_messages*.properties.
Entity type metadata
GET /api/admin/audit-log/entity-types returns List<AuditEntityTypeDto>:
[
{
"code": "AppPortEntity",
"group": "ACS",
"label": "App port",
"icon": "database",
"supportsSnapshot": true,
"supportsDiff": true
}
]
Entity type registry
AuditEntityTypeRegistry implements BeanPostProcessor and scans every bean at startup for methods carrying @Auditable.
For each annotated method, the entity simple name, group, label key, icon, snapshot support, and diff support are recorded.
If multiple @Auditable methods reference the same entity, the first registration carrying explicit metadata wins.
The registry currently tracks 54 entity types across 6 groups:
-
ACS — bulk data, CTN info, external trace, FCC, FT ACS task, FT ACS WS, hardcoded event, parameter names cache, set parameter, configuration
-
Provision Portal — CSV settings, custom status, objects
-
Northbound API — northbound configuration
-
Service API — service API configuration
-
Support/Management Portals — Angular columns, custom params, tabs, app ports
-
User Management — users, roles, sessions
Actor / Target separation
Audit DTOs separate the actor (who performed the action) from the target (what entity was affected):
AuditActorDto
{ "id": 1, "username": "admin" }
AuditTargetDto
{ "id": "42", "type": "UserEntity", "displayName": "john.doe" }
displayName is resolved at read time (e.g., looking up the username for UserEntity targets).
It is null when the entity cannot be resolved.
Both DTOs appear in AuditEventSummaryDto (list view) and AuditEventDetailDto (detail view).
Preview changes in list view
AuditEventSummaryDto includes previewChanges — the first 3 field-level changes for each event.
These are batch-fetched per page to avoid N+1 queries.
The frontend can display an inline preview (e.g., “description: old → new”) without loading the full detail view.
Persistence Model
Tables
| Table | Entity | Purpose |
|---|---|---|
|
|
One row per audited operation. Stores who, what, when, and context metadata. |
|
|
Deduplicated JSON snapshots of entity state. Shared across events via content-hash. |
|
|
Junction table linking events to snapshots with a role ( |
|
|
Field-level change records for UPDATE operations (one row per changed field). |
Timestamps
All timestamps (occurred_at, created_at) use java.time.Instant and are persisted in UTC.
The API returns timestamps in ISO-8601 format with a Z suffix (e.g., 2026-01-15T10:30:00Z).
Snapshot deduplication
Snapshots are deduplicated by content hash to avoid storing identical entity states multiple times:
-
Entity state is serialized to JSON.
-
SHA-256 hash is computed and Base64-encoded (
content_hash). -
If a snapshot with the same hash already exists, its
ref_countis incremented and the existing snapshot is reused. -
If no match, a new snapshot is created with
ref_count = 1. -
When an event is deleted,
ref_countis decremented. Snapshots withref_count = 0are automatically removed.
An in-memory LRU cache (max 1000 entries) eliminates database lookups for frequently occurring entity states.
Field-level change tracking
For UPDATE operations, the system records individual field changes via AuditChangeEntity:
| Operation | old_value | new_value | field_path |
|---|---|---|---|
CREATE |
null |
full JSON |
|
DELETE |
full JSON |
null |
|
UPDATE |
field value |
field value |
one row per changed field (e.g., |
Field path notation supports:
-
Simple fields:
description,status -
Nested objects:
address.city,config.timeout -
Array elements:
ports[0].protocol,tags[2] -
Full entity marker:
$entity(used for CREATE/DELETE)
Sensitive field redaction
SensitiveFieldRedactor masks sensitive values in snapshots and field-level changes before they are returned by the API.
The original data remains untouched in the database for compliance purposes.
How it works
-
Operates on the serialized JSON, not on the entity.
-
Replaces values of known-sensitive fields with
<redacted>. -
Matches by the last segment of dot-notation paths:
config.passwordmatchespassword. -
Array indices are stripped before matching:
items[0].apiKeymatchesapiKey. -
$entitychanges (CREATE/DELETE) are also redacted — the full-entity JSON is processed recursively.
Default sensitive fields
The following 18 field names are redacted by default:
password, passwordHash, password_hash, apiKey, api_key, secret, clientSecret, client_secret, token, refreshToken, refresh_token, accessToken, access_token, mailPassword, mail_password, dbPassword, db_password, credentials
Audit retention
AuditRetentionService runs a daily scheduled cleanup job at 03:00 server time.
Configuration
| Property | Default | Range | Description |
|---|---|---|---|
|
|
3—90, or 0 |
Number of days to keep audit events. Set to |
Environment variable: AUDIT_RETENTION_DAYS
Behavior
-
Values below 3 are clamped to 3 with a warning log.
-
Values above 90 are clamped to 90 with a warning log.
-
When set to
0, the cleanup job runs but immediately returns without deleting anything. -
Cascade rules on
AuditEventEntityhandle deletion of relatedaudit_changeandaudit_event_snapshotrows. -
Orphaned snapshots (
ref_count = 0) are removed automatically.
CSV export
GET /api/admin/audit-log/export streams audit events as a CSV download.
Columns
The export contains 15 columns:
id, occurredAt, operation, actor, actorId, entityType, entityId, targetDisplayName, context, changeReason, changedFields, changesSummary, transactionId, ip, userAgent
Enrichment
-
targetDisplayNameis resolved at export time (e.g., username lookup forUserEntitytargets). -
changedFieldslists all changed field paths, semicolon-separated. -
changesSummaryshows the first 5 changes infieldPath: oldValue -> newValueformat, with values truncated to 50 characters. -
Sensitive field values in
changesSummaryare replaced with<redacted>.
Change reason tracking
The changeReason field captures why a change was made.
It is populated automatically for authentication-related operations:
-
Locale change — records the locale transition
-
Password change — records that a password was changed
-
Role change — records old and new role
-
First login — records that the user completed first-time login
For configuration operations, changeReason is set via the @Auditable annotation’s changeReason SpEL expression, typically from a request DTO field.
Processing Flow (post-commit)
-
A service method annotated with
@Auditablecompletes successfully inside a database transaction (TX1). -
Before the method executes, the aspect captures a BEFORE snapshot of the entity state (if configured).
-
After the method returns, the aspect captures an AFTER snapshot.
-
An
AuditEventis published via Spring’sApplicationEventPublisher. -
After TX1 commits,
AuditEventListenerhandles the event using@TransactionalEventListener(phase = AFTER_COMMIT, fallbackExecution = true). -
The event is submitted to a bounded async executor (
AuditConfig#auditExecutor) — this does not block the request thread. -
AuditLogWriter.persistruns in a new transaction (Propagation.REQUIRES_NEW) so audit persistence does not interfere with the original business transaction. -
During persistence:
-
Snapshots are deduplicated via
AuditSnapshotService.findOrCreateSnapshot. -
Field-level changes are computed by
AuditFieldChangeTracker.computeChanges. -
Sensitive fields in snapshots and changes are redacted before returning to the API (not at write time).
-
The event entity is saved with cascading persistence to junction and change records.
-
Request Correlation
AuditRequestContextFilter stores per-request metadata:
-
X-Request-Idheader (generated when missing) -
client IP (
HttpServletRequest.getRemoteAddr()) -
user agent
These values are written into audit_event.request_id, ip, user_agent.
The transaction_id field groups related operations within the same logical transaction (derived from request_id or auto-generated as tx-xxx).
REST API summary
All endpoints require the users.manage authority and are prefixed with /api/admin/audit-log.
| Method | Path | Description |
|---|---|---|
|
|
Paginated list of audit events with filters ( |
|
|
Full event detail with all changes, before/after snapshots. Returns |
|
|
List of entity type metadata for filter dropdowns. Returns |
|
|
List of operation metadata with severity and destructive flag. Returns |
|
|
Streaming CSV export with all filters. Max 100,000 rows. |
Key source files
| Package / File | Purpose |
|---|---|
|
REST endpoints for listing, detail, export, and metadata |
|
Business logic for event queries and DTO mapping |
|
Chunked CSV export with batch-fetched changes and resolved target names |
|
Scheduled daily cleanup of old events |
|
Masks sensitive values in JSON snapshots and change records |
|
Startup scanner that catalogs all |
|
Record holding per-entity-type metadata (group, icon, snapshot/diff flags) |
|
Operation metadata DTO (code, label, severity, destructive) |
|
Entity type metadata DTO (code, group, label, icon, supportsSnapshot, supportsDiff) |
|
Actor DTO (id, username) |
|
Target DTO (id, type, displayName) |