-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Summary
Add opt-in support for creating audit records when a user views entity records. Currently audits only cover insert, update, and delete operations. Some use cases require tracking read access for compliance, security, or usage analytics.
Proposed Behavior
- Opt-in at the same level that all audits are currently opted into (not a separate table-level flag)
- When enabled, a GET/view of a single record creates an audit entry
- Explore adding "table-level audits" that would cover both individual record views and list/query views on a table
- Audit detail level should be configurable (e.g., just the fact of the view vs. which fields were accessed)
Considerations
- Performance impact of auditing reads (far more frequent than writes)
- Should integrate with the existing pluggable audit handler system (PR feat(audit): add pluggable audit handler system #356)
- Table-level audit scope: individual record views vs. list/query views may warrant different audit granularity
- May want to support additional optional audit types in the future beyond just "view"
ADR: Design and Approach
Decision: Separate readAuditLevel field on QAuditRules
Read vs write audit granularity are independent dimensions. A table may want FIELD-level write auditing but only GET-level read auditing. Mixing these into a single enum creates combinatorial explosion. Adding a separate readAuditLevel field keeps them orthogonal.
Rejected alternatives:
- Adding to
AuditLevelenum -- conflates two dimensions, breaks single-responsibility - Separate
QReadAuditRulesclass -- fragments configuration, users expect one place for audit config
New Enum: ReadAuditLevel
NONE -- No read auditing (default)
GET -- Audit single-record views only
GET_AND_QUERY -- Audit both individual views and table queries
Extend QAuditRules
Add private ReadAuditLevel readAuditLevel; field. Defaults to null (treated as NONE).
new QAuditRules()
.withAuditLevel(AuditLevel.FIELD)
.withReadAuditLevel(ReadAuditLevel.GET)New Action: ReadAuditAction
Static convenience method executeAsync() called from GetAction/QueryAction. Captures QContext, submits to AuditHandlerExecutor thread pool. Never blocks or fails the read operation.
For GET (single record view):
- One audit record per viewed record
audit.message= "Record was Viewed"audit.recordId= primary key of viewed record- No auditDetail records
For QUERY (table/list view):
- One audit record per returned record
audit.message= "Record was included in Query result"audit.recordId= primary key of each returned record- No auditDetail records
Performance (critical for queries):
executeAsync()is fire-and-forget: captures only primary keys + QContext, submits to thread pool, returns immediately. Query response goes back to UI before any audit I/O occurs.- Background thread builds all audit records in memory, then does a single bulk
InsertAction(1 DB call, not N). - For piped/streaming queries: collect primary keys during pipe flow, audit once after pipe flushes.
- Only primary keys are captured, not full QRecord objects, keeping memory footprint small.
New Handler Type: AuditHandlerType.READ
Add READ to the existing AuditHandlerType enum. New ReadAuditHandlerInterface with handleReadAudit(ReadAuditHandlerInput). Allows custom read audit processing (e.g., streaming to external compliance systems).
Integration Points
GetAction -- after postRecordActions(), fire async read audit if record found.
QueryAction -- after postRecordActions(), fire async read audit. For piped queries, audit fires once after pipe flushes.
InputSource guard: Read audits only fire when InputSource == QInputSource.USER. System/process reads are not audited. This prevents audit cascading and ensures only user-facing operations are tracked.
Recursion Prevention
Two layers: (1) InputSource guard -- system reads during audit processing have SYSTEM input source, (2) audit system tables have no readAuditLevel configured.
Storage
Reuses existing audit/auditDetail tables. No schema changes. Read vs write audits distinguished by message text ("Viewed" / "included in Query result" vs "Inserted/Edited/Deleted").
Files to Create
| File | Description |
|---|---|
ReadAuditLevel.java |
Enum: NONE, GET, GET_AND_QUERY |
ReadAuditAction.java |
Core action: checks rules, builds audit records, fires async |
ReadAuditInput.java |
Input model for ReadAuditAction |
ReadAuditHandlerInterface.java |
Handler interface for custom read audit processing |
ReadAuditHandlerInput.java |
Input passed to read audit handlers |
ReadAuditActionTest.java |
Unit tests |
Files to Modify
| File | Change |
|---|---|
QAuditRules.java |
Add readAuditLevel field |
AuditHandlerType.java |
Add READ value |
AuditHandlerExecutor.java |
Add executeReadHandlers() |
GetAction.java |
Add async read audit call |
QueryAction.java |
Add async read audit call |
QInstanceValidator.java |
Validate READ handlers + readAuditLevel consistency |
Backwards Compatibility
Fully backwards compatible. readAuditLevel defaults to null/NONE. No schema changes. No new dependencies.
Metadata
Metadata
Assignees
Labels
Type
Projects
Status