Audit Trail — Forms
Audit Trail — Forms & Input Specification
Reference technical blueprint: audit.md 5 sub-modules · 11 forms · the immutable, append-only event log + two-key cosign workbench of Aayojana.
The Audit Trail module is cross-cutting infrastructure. Other modules (Statutory, Compliance, Vitta, Asset Management, Members Suite, Staff) call into it; it never calls them. The forms below split into two human-facing flows — the primary actor who proposes a legal-weight change, and the second signer who reviews and either cosigns or rejects — plus admin-only forms for configuring coverage rules, searching the event log, and triaging suspicious activity.
A foundational principle: most ordinary CRUD does not need a form here at all. The SQLAlchemy event-listener bridge auto-emits an AuditEvent whenever a __audit_log__ = True model is flushed. The forms in this module are for the exception path — when a write requires a reasoned, two-keyed approval; when an admin needs to investigate after-the-fact; or when a coverage rule needs to be tuned.
A self-veto rule applies throughout: the user who submits a PendingWrite cannot also be the cosigner. Enforced at service layer with HTTP 403.
Sub-module 1: Two-Key Submit (Primary Actor)
Form 1.1 — Two-Key Pending Write Submission
Purpose: The form a primary actor sees when proposing a change to a covered entity. This isn't a standalone page — it's the edit form for any legal-weight entity (Trust Registration edit, Bank Loan create, Audit Engagement edit, etc.) augmented with a mandatory Reason textarea and a "Submit for cosign" button instead of a "Save" button. Specified here as a meta-form whose fields appear at the bottom of every two-key edit screen. Who fills: Any user with edit rights on the underlying entity. Frequency varies — some tenants see this several times per week (filings, custody moves), others rarely (trust deed amendment).
| Field | Type | Required | Options / Validation | Default | Help |
|---|---|---|---|---|---|
| (Entity-specific fields) | varies | varies | per the source form (Statutory / Compliance / Vitta / etc.) | — | The actual change — e.g. new locker_id on a custody record |
| Submission Reason | Textarea | Yes | min 20 chars; max 4000 | — | The narrative the cosigner will read. Must explain why, not just what. Visible in /audit/pending queue and in the final audit event |
| Eligible Cosigner Hint | Read-only text | No | Auto-derived from coverage rule | computed | Shows which roles can cosign — e.g. "Any tenant-admin (excluding you)" |
| Expiry Hint | Read-only text | No | Auto-derived from coverage rule | computed | "This pending write expires in 168 hours (7 days) if not cosigned" |
| Race-detection Snapshot | Hidden field | Yes (system) | — | auto-captured | The current row state at submission — used to detect concurrent edits |
| Acknowledge Two-Key | Checkbox | Yes | Must tick to submit | unchecked | "I understand this change requires a second authorised user to cosign before it takes effect" |
Submit action: Calls submit_two_key_change(entity_type, entity_id, action, changes, actor_user_id, reason, before_snapshot, db). Creates a PendingWrite row with status='pending', expires_at = now + cosign_window_hours. Also writes a submit AuditEvent (action='submit'). Returns a confirmation page with a link to /audit/pending/{id} and a notice that eligible cosigners have been notified via Comms.
Related forms: Two-Key Cosign Form (2.1) is what the second signer sees next.
(Workflow note: From the actor's standpoint, the only difference from a normal edit is the Reason textarea, the acknowledgement checkbox, and the post-submit "Submitted for cosign" message instead of "Saved". The change does NOT take effect until cosign — the live row remains unchanged. The actor receives an email when their submission is cosigned, rejected, or expired.)
Sub-module 2: Two-Key Cosign (Second Signer)
Form 2.1 — Two-Key Cosign / Reject Form
Purpose: The form a second signer sees when reviewing a pending write. Shows a side-by-side diff (before / after, changed columns highlighted), the submitter's reason, and offers Cosign or Reject. The single most consequential form in Aayojana — every cosign here is a legal commitment.
Who fills: Any user whose role is in the coverage rule's eligible_cosigner_roles AND who is not the submitter. Typically tenant-admins.
| Field | Type | Required | Options / Validation | Default | Help |
|---|---|---|---|---|---|
| Pending Write ID | Read-only | system | — | from URL | The PendingWrite.id |
| Entity Type | Read-only | system | — | from PendingWrite | e.g. trust_registrations, bank_loans, compliance_filings |
| Entity ID | Read-only | system | — | from PendingWrite | The target row's PK; null for create-actions |
| Action | Read-only | system | — | from PendingWrite | create / update / delete |
| Submitted By | Read-only | system | — | from PendingWrite | Submitter's name, role, email |
| Submitted At | Read-only | system | — | from PendingWrite | Timestamp + relative ("3 hours ago") |
| Submitted Reason | Read-only | system | — | from PendingWrite | The narrative the submitter wrote — read carefully |
| Diff Preview | Read-only | system | Two-column table; changed cols amber-highlighted; PII redacted to last-4 unless viewer has view-pii |
from before_snapshot + changes | The actual proposed change |
| Race-Detection Status | Read-only | system | green ("snapshot still matches live") / red ("live row has changed since submission") | computed at page load | If red, cosign is blocked — submitter must resubmit |
| Expiry Countdown | Read-only | system | — | computed from PendingWrite.expires_at | "Expires in 4d 3h" |
| Decision | Radio | Yes | cosign / reject | — | The actual decision |
| Cosign Note | Textarea | Conditional | min 10 chars when decision=cosign | — | Optional context — "verified against original deed in locker A-117" |
| Rejection Reason | Textarea | Conditional | min 30 chars when decision=reject; required | — | Mandatory if rejecting — e.g. "EC date doesn't match the underlying record" |
| Confirm I Am Not the Submitter | Checkbox | Yes | Must tick — system also checks server-side | unchecked | Self-veto safeguard surfaced explicitly |
Submit action — Cosign:
1. Calls cosign_pending_change(pending_id, second_signer_user_id, note, db).
2. Validates: status==pending, signer != submitter, signer role in eligible_cosigner_roles, not expired, before_snapshot still matches live row.
3. Applies changes to the target row.
4. Writes the final AuditEvent with action matching the original (create/update/delete), pending_write_id linking back, and second_signer_user_id set.
5. Updates PendingWrite.status='cosigned', cosigned_by_user_id, cosigned_at, cosign_note.
6. Appends AuditSignoffLog row with decision='cosign'.
7. Notifies submitter via Comms.
Submit action — Reject:
1. Calls reject_pending_change(pending_id, rejecter_user_id, reason, db).
2. Sets PendingWrite.status='rejected', rejected_reason.
3. Writes AuditEvent with action='reject'.
4. Appends AuditSignoffLog row with decision='reject'.
5. Notifies submitter via Comms — the submitter can re-submit a corrected version.
Related forms: Originated from any two-key submit form (1.1). After cosign, the entity-history sidebar of the underlying detail page now shows the new event.
Sub-module 3: Audit Coverage Configuration
Form 3.1 — Coverage Rule Add / Edit
Purpose: Admin-only form to declare which (entity_type, action, column) tuples require two-key, and which simply log passively. Most rules are seeded globally in migration 0013; tenant-specific overrides can tighten (never loosen below global).
Who fills: Tenant-admin (for tenant-specific rows); super-admin only (for global rows). Edits are infrequent and tightly audited.
| Field | Type | Required | Options / Validation | Default | Help |
|---|---|---|---|---|---|
| Scope | Dropdown | Yes | global / tenant-specific | tenant-specific | Global rows have tenant_id=NULL. Only super-admin can create global rules |
| Tenant | FK (Organisations) | Conditional | Required when Scope=tenant-specific | logged-in tenant | — |
| Entity Type | Text (64) | Yes | DB table name OR logical name (e.g. jewel_vault); auto-complete from registered models |
— | What table or logical entity this rule covers |
| Action | Dropdown | Yes | create / update / delete / * (all) | update | The verb the rule applies to |
| Column Name | Text (64) | No | Must be a real column on the entity if Action=update | — | If set, two-key triggers only when this column is in the diff. Leave blank for "any column" |
| Requires Two-Key | Checkbox | Yes | — | unchecked | Tick to require cosign |
| Requires Reason | Checkbox | Yes | — | checked when Two-Key | Forces a Reason textarea even on passive log |
| Cosign Window Hours | Number | Yes | 1 – 720 (30 days max) | 168 (7 days) | How long pending writes wait before expiring |
| Eligible Cosigner Roles | Multi-select | No | tenant-admin / module-admin / branch-admin / super-admin | ["tenant-admin"] |
Which roles can cosign. Empty = any tenant-admin |
| Notes | Textarea | No | — | — | Why this rule exists — visible to other admins |
| Reason for Change | Textarea | Yes on edit/delete | min 30 chars | — | Coverage rules are themselves audit-logged; explain why the rule is being added/changed |
Validation rules:
- A tenant-specific row can only tighten a global rule (e.g. add two-key where global says no, narrow eligible cosigners). It cannot disable two-key that the global default has set.
- requires_reason=true is mandatory when requires_two_key=true.
- Removing a rule is two-key itself (meta!).
Submit action: Creates / updates audit_coverage row. Reloads the in-memory cache so changes take effect on next request. Audit-logged.
Related forms: Coverage list page surfaces all rules grouped by entity_type.
Form 3.2 — Coverage Rule Removal
Purpose: Removes a tenant-specific override (cannot remove globals). Who fills: Tenant-admin. Two-key.
| Field | Type | Required | Options / Validation | Default | Help |
|---|---|---|---|---|---|
| Coverage Rule | FK (Audit Coverage) | Yes | Must be tenant-specific (tenant_id != NULL) |
from page context | — |
| Removal Reason | Textarea | Yes | min 50 chars | — | Why the override is no longer needed |
| Acknowledge Reverts to Global Default | Checkbox | Yes | — | unchecked | "I understand removing this override means the global default applies" |
Submit action: Two-key. On cosign, deletes the row. Reloads coverage cache. Audit-logged. Related forms: Surfaces in coverage admin grid with a "Reverts to global" indicator.
Sub-module 4: Audit Event Search & Investigation
Form 4.1 — Audit Event Search / Filter
Purpose: Read-only investigation form — search the immutable event log by entity, by user, by date, by action, by request-id. Used by tenant-admins for self-audit and by external auditors during fieldwork. Who fills: Tenant-admin or module-admin. Frequent — daily during audit season.
| Field | Type | Required | Options / Validation | Default | Help |
|---|---|---|---|---|---|
| Entity Type | Dropdown | No | All registered audit entity_types | all | Filter by table/logical entity |
| Entity ID | Text (64) | No | — | — | Specific row's PK |
| Action | Multi-select | No | create / update / delete / view-pii / period-lock-override / fcra-transfer / cosign / reject / export / submit | all | What action(s) to include |
| Actor (User) | FK (Users) | No | — | — | Who performed the action |
| Second Signer | FK (Users) | No | — | — | Who cosigned (only relevant for two-key events) |
| Date Range | Date range | No | — | last 30 days | When the action happened |
| Provider | Dropdown | No | aayojana.statutory / aayojana.compliance / aayojana.vitta / aayojana.assets / aayojana.members / aayojana.staff / aayojana.comms / aayojana.audit | all | Which module emitted the event |
| Request ID | Text (64) | No | — | — | Trace a single HTTP request across modules |
| Reason Contains | Text | No | — | — | Substring search inside reason text |
| Diff Contains Column | Text (64) | No | — | — | Filter to events where diff_keys includes this column name |
| Has PII Access | Checkbox | No | — | unchecked | Show only action='view-pii' events |
| Sort By | Dropdown | Yes | newest first / oldest first / by actor / by entity | newest first | — |
| Page Size | Number | Yes | 25 / 50 / 100 / 250 | 50 | — |
Submit action: Issues GET /api/audit/events with filter params. Returns paginated table. Read-only.
Related forms: Each row links to Event Detail (4.2).
Form 4.2 — Audit Event Detail View
Purpose: Single-event drill-down showing full before/after blobs, actor context, request metadata, and tamper-evidence checksum. Who fills: Same as 4.1. Read-only.
| Field | Type | Required | Options / Validation | Default | Help |
|---|---|---|---|---|---|
| Event ID | Read-only | system | UUID | from URL | — |
| Provider | Read-only | system | — | from row | Which module |
| Entity Type / ID | Read-only | system | — | from row | The target row |
| Action | Read-only | system | — | from row | — |
| Actor | Read-only | system | — | from row | Submitter — name + role + email |
| Second Signer | Read-only | system | — | from row | Cosigner if applicable |
| Reason | Read-only | system | — | from row | The narrative |
| Before Blob | Read-only | system | JSON; PII redacted to last-4 unless view-pii granted |
from row | Full pre-state for INSERT/DELETE; diff for UPDATE |
| After Blob | Read-only | system | JSON; same redaction rule | from row | Full post-state |
| Diff Keys | Read-only | system | Array | from row | Quick list of changed columns |
| Request ID | Read-only | system | — | from row | Trace ID |
| IP Address | Read-only | system | — | from row | — |
| User Agent | Read-only | system | — | from row | — |
| Pending Write Ref | Read-only | system | Link if present | from row | Click-through to the original PendingWrite record |
| Row Hash | Read-only | system | SHA-256 hex; tamper-evidence chain | from row (filled nightly) | Click to verify against the chain |
| Reveal PII (button) | Action | No | requires view-pii privilege |
— | Clicking emits its own AuditEvent with action='view-pii' |
Submit action: None (read-only). The "Reveal PII" button issues a view-pii event to the audit log itself.
Related forms: Linked from Event Search (4.1) and from the Entity History Sidebar (4.3).
Form 4.3 — Entity History Sidebar (embedded)
Purpose: Inline biographical view of all events for a single entity row — embedded as a sidebar on Statutory, Compliance, Vitta, Asset Management detail pages. Specified here as a form-style read-only filter even though it's not a primary-form per se. Who fills: Anyone viewing the entity's detail page. Read-only.
| Field | Type | Required | Options / Validation | Default | Help |
|---|---|---|---|---|---|
| Entity Type / ID | Read-only | system | — | from page context | Locked to the host page |
| Action Filter | Multi-select | No | create / update / delete / view-pii / cosign / reject | all | Quick toggles |
| Show Last N | Number | Yes | 5 / 20 / 100 / all | 20 | — |
| Show PII Reveals Only | Checkbox | No | — | unchecked | — |
Submit action: Reloads the sidebar (no DB write). Related forms: Click an entry → Event Detail (4.2).
Sub-module 5: Suspicious-Activity Review
Form 5.1 — Suspicious-Activity Flag Review
Purpose: Triage form for activity flagged by heuristic rules — same actor making >N two-key submissions in a 24-hour window, after-hours edits to legal-weight tables, bulk-deletes, or PII-read spikes. Produces an investigation outcome that is itself audit-logged. Who fills: Tenant-admin. Daily review when flags are present.
| Field | Type | Required | Options / Validation | Default | Help |
|---|---|---|---|---|---|
| Flag ID | Read-only | system | — | from row | Internal flag identifier |
| Heuristic Triggered | Read-only | system | — | from row | e.g. Bulk-delete: 47 rows in 3 minutes by user X, After-hours custody edit at 02:14, PII reveal spike: 12 Aadhaar reads in 1 hour |
| Detected At | Read-only | system | Timestamp | from row | — |
| Actor (User) | Read-only | system | — | from row | The user whose activity tripped the heuristic |
| Related Events Count | Read-only | system | Integer with link | from row | Click → pre-filtered Event Search (4.1) |
| Related Events Preview | Read-only | system | Mini-table of top 10 events | from query | Inline preview |
| Severity | Read-only | system | low / medium / high / critical | from heuristic | — |
| Investigation Outcome | Dropdown | Yes | benign / explained / escalate / confirmed-incident | — | The triager's decision |
| Investigation Notes | Textarea | Yes | min 30 chars | — | What was checked, what was found, why this outcome |
| Escalation Recipient | FK (Users) | Conditional | Required when Outcome=escalate | trust chairperson | Who to notify on escalation |
| Lock Account (action) | Checkbox | No | requires Outcome=confirmed-incident | unchecked | Force-logout the actor and disable login (super-admin only) |
| Reason | Textarea | Yes | min 30 chars | — | Audit-logged with the outcome |
Submit action: Writes an AuditEvent (action='suspicious-flag-reviewed') with the outcome. If Outcome=escalate, dispatches an email + WhatsApp via Comms to the escalation recipient. If Lock Account is ticked (and reviewer is super-admin), calls users.disable_login(actor_user_id, reason).
Related forms: Cross-references back into Event Search (4.1).
Form 5.2 — Suspicious-Activity Heuristic Configuration
Purpose: Tenant-admin tuning of heuristic thresholds — e.g. raise the "bulk delete" threshold from 50 to 100 if a tenant has legitimately high-volume deletes (e.g. event cleanup). Who fills: Tenant-admin. Rare. Two-key on changes.
| Field | Type | Required | Options / Validation | Default | Help |
|---|---|---|---|---|---|
| Heuristic Code | Dropdown | Yes | bulk-delete / after-hours-edit / pii-read-spike / two-key-submission-spike / new-cosigner-pattern / custody-move-spike | — | Which heuristic to tune |
| Threshold Value | Number | Yes | per heuristic (e.g. count, hours, percentage) | system default | — |
| Lookback Window (hours) | Number | Yes | 1 – 168 | 24 | — |
| After-Hours Definition | Time range | Conditional | Required when Heuristic=after-hours-edit | 22:00 – 06:00 (tenant local) | — |
| Severity | Dropdown | Yes | low / medium / high / critical | medium | Affects dashboard sort |
| Is Active | Toggle | Yes | true / false | true | — |
| Reason | Textarea | Yes | min 30 chars | — | Two-key reason |
Submit action: Two-key. On cosign, updates heuristic config (stored in audit_coverage-style config or a dedicated audit_heuristics table — implementation detail). Reloads detector cache.
Related forms: Suspicious-Activity Flag Review (5.1) uses the resulting thresholds.
(Module-wide workflow note: The Audit module's day-to-day is dominated by the cosign queue. A tenant-admin logs in, sees N pending writes on the dashboard, walks the queue using Form 2.1 (cosign/reject one at a time, reading the diff and reason). They glance at the suspicious-activity tile (Form 5.1) and triage anything yellow or red. Once a quarter — or after a regulatory event — they use Form 4.1 to extract a date-bounded slice of the event log for the auditor handoff (CSV export). Coverage rules (3.1, 3.2) and heuristic tuning (5.2) are quarterly-or-rarer activities. The module's primary value isn't the forms themselves — it's the automatic recording that happens via the SQLAlchemy bridge whenever any other module writes a covered row. The forms here are the human surface on top of an immutable substrate.)