18
🔒

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