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

CREATE

LOW

No

Entity created

UPDATE

LOW

No

Entity modified

DELETE

HIGH

Yes

Entity removed

IMPORT

MEDIUM

No

Bulk import from file

READ

LOW

No

Sensitive read recorded

OTHER

LOW

No

Uncategorized action

PASSWORD_CHANGE

MEDIUM

No

User password changed

SESSION_REVOKE

HIGH

Yes

Session revoked

ROLE_CHANGE

HIGH

No

User role changed

USER_SUSPEND

HIGH

Yes

User account suspended

USER_ACTIVATE

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

audit_event

AuditEventEntity

One row per audited operation. Stores who, what, when, and context metadata.

audit_snapshot

AuditSnapshotEntity

Deduplicated JSON snapshots of entity state. Shared across events via content-hash.

audit_event_snapshot

AuditEventSnapshotEntity

Junction table linking events to snapshots with a role (BEFORE or AFTER).

audit_change

AuditChangeEntity

Field-level change records for UPDATE operations (one row per changed field).

Entity Relationships

Diagram

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:

  1. Entity state is serialized to JSON.

  2. SHA-256 hash is computed and Base64-encoded (content_hash).

  3. If a snapshot with the same hash already exists, its ref_count is incremented and the existing snapshot is reused.

  4. If no match, a new snapshot is created with ref_count = 1.

  5. When an event is deleted, ref_count is decremented. Snapshots with ref_count = 0 are 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

$entity (single row)

DELETE

full JSON

null

$entity (single row)

UPDATE

field value

field value

one row per changed field (e.g., description, address.city, ports[0].protocol)

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.password matches password.

  • Array indices are stripped before matching: items[0].apiKey matches apiKey.

  • $entity changes (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

Adding custom fields

Set audit.redaction.fields (comma-separated) to extend the default list:

audit:
  redaction:
    fields: "myCustomSecret,internalToken"

Or via environment variable:

AUDIT_REDACTION_FIELDS=myCustomSecret,internalToken

Audit retention

AuditRetentionService runs a daily scheduled cleanup job at 03:00 server time.

Configuration

Property Default Range Description

audit.retention.days

30

3—​90, or 0

Number of days to keep audit events. Set to 0 to disable retention (keep all events indefinitely).

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 AuditEventEntity handle deletion of related audit_change and audit_event_snapshot rows.

  • 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

  • targetDisplayName is resolved at export time (e.g., username lookup for UserEntity targets).

  • changedFields lists all changed field paths, semicolon-separated.

  • changesSummary shows the first 5 changes in fieldPath: oldValue -> newValue format, with values truncated to 50 characters.

  • Sensitive field values in changesSummary are replaced with <redacted>.

Limits

Maximum 100,000 rows per export. If the result set exceeds this limit, the endpoint returns HTTP 413 Payload Too Large.

Filters

All list-view filters are supported: operation, entityType, username, from, to.

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)

  1. A service method annotated with @Auditable completes successfully inside a database transaction (TX1).

  2. Before the method executes, the aspect captures a BEFORE snapshot of the entity state (if configured).

  3. After the method returns, the aspect captures an AFTER snapshot.

  4. An AuditEvent is published via Spring’s ApplicationEventPublisher.

  5. After TX1 commits, AuditEventListener handles the event using @TransactionalEventListener(phase = AFTER_COMMIT, fallbackExecution = true).

  6. The event is submitted to a bounded async executor (AuditConfig#auditExecutor) — this does not block the request thread.

  7. AuditLogWriter.persist runs in a new transaction (Propagation.REQUIRES_NEW) so audit persistence does not interfere with the original business transaction.

  8. 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-Id header (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

GET

/

Paginated list of audit events with filters (operation, entityType, username, from, to, search). Returns Page<AuditEventSummaryDto> with actor, target, and preview changes.

GET

/{eventId}

Full event detail with all changes, before/after snapshots. Returns AuditEventDetailDto.

GET

/entity-types

List of entity type metadata for filter dropdowns. Returns List<AuditEntityTypeDto>.

GET

/operations

List of operation metadata with severity and destructive flag. Returns List<AuditOperationDto>.

GET

/export

Streaming CSV export with all filters. Max 100,000 rows.

Key source files

Package / File Purpose

audit/controller/AuditLogController

REST endpoints for listing, detail, export, and metadata

audit/service/AuditLogService

Business logic for event queries and DTO mapping

audit/service/AuditExportService

Chunked CSV export with batch-fetched changes and resolved target names

audit/service/AuditRetentionService

Scheduled daily cleanup of old events

audit/redaction/SensitiveFieldRedactor

Masks sensitive values in JSON snapshots and change records

audit/registry/AuditEntityTypeRegistry

Startup scanner that catalogs all @Auditable entity types

audit/registry/AuditEntityTypeMetadata

Record holding per-entity-type metadata (group, icon, snapshot/diff flags)

audit/dto/AuditOperationDto

Operation metadata DTO (code, label, severity, destructive)

audit/dto/AuditEntityTypeDto

Entity type metadata DTO (code, group, label, icon, supportsSnapshot, supportsDiff)

audit/dto/AuditActorDto

Actor DTO (id, username)

audit/dto/AuditTargetDto

Target DTO (id, type, displayName)