Skip to content

record_views lists an ip_address column that no read-path writer ever stamps — a declared-but-unwritten column on a shipped compliance view #9539

Description

@os-project-manager

Found while documenting the record-view audit surface for #9517 (PR pending on branch claude/issue-9517-readme-read-audit). Not fixed there — that PR is docs-only and this is a code change to a landed feature, so it does not meet the in-place bar.

What is wrong

packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts declares the record_views list view with these columns:

columns: ['created_at', 'user_id', 'object_name', 'record_id', 'ip_address'],
filter: [{ field: 'action', operator: 'in', value: ['read'] }],

The view is filtered to action: 'read' rows only. But the writer that produces those rows — buildRow in packages/plugins/plugin-audit/src/read-audit.ts — sets exactly:

action, created_at, user_id, object_name, record_id, old_value, new_value, tenant_id, and conditionally organization_id and actor.

ip_address is not among them, and no other writer emits a read row. ⇒ The last column of this view is empty on every row it can ever show.

Reproduce without running anything:

git grep -n "ip_address" -- packages/plugins/plugin-audit/src/read-audit.ts # no hits
git grep -n "'ip_address'" -- packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts

The second prints the record_views column list. The first prints nothing.

Why it is worth a card

This is the same defect class #7675 / #8147 / #8315 spent three cards closing on this exact object, one layer down: those retired enum values with no writer; this is a column with no writer, on the view shipped to surface the newest audit action. The stated principle there was 审计面宁窄勿谎 — a compliance surface should be narrow rather than untruthful — and an always-blank IP Address column on the "who viewed this record" screen reads to an auditor as "we captured the client fingerprint and this request had none", which is a stronger and wrong claim.

It is also live on the same screen plugin-audit contributes into the Setup app's group_diagnostics navigation.

Note on the existing field semantics

ip_address / user_agent on sys_audit_log are populated on auth events only — the record-level CRUD writer does not stamp them either. So the column is not "broken for reads"; it was never a column a non-auth row carries. The record_views view is the only place a non-auth-filtered view puts it in the column list.

Options (not deciding)

  1. Drop ip_address from the record_views columns. Smallest change; makes the view show only what the row carries. Candidate replacement column: actor, which the read writer does stamp and which is the one that attributes a service principal.
  2. Stamp it in the read writer. The afterFind hook has ctx.session; whether a client fingerprint is reachable there — and whether an audit ledger of views should carry one — is a product call, not a mechanical fix. ⚠️ Note the read writer is deliberately allocation-only on the request path, so anything added there is on the read's critical section.

⇒ Option 1 unless someone wants the capability; the point of this card is that the current state claims a capability neither option has delivered yet.

Refs: #9517 (the README card that surfaced it) · #8992 / PR #9515 (the read-audit MVP) · #7675 / #8147 / #8315 (the same defect class on the enum).

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions