16
🔒

Audit Trail — Forms

Two-key trail for legal-weight entities

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.)