Skip to content

Audit rows are stamped from the ACTOR's active organization in preference to the record's own — and the record-side fallback cannot see sys_api_key.active_organization_id #8707

Description

@qq9340100

Filed from #8287 (PR: claude/issue-8287-api-key-organization). This is the unimplemented half of that card's follow-through decision 2 — the decision itself was already taken by the maintainer; what this issue records is that its implementation site is not where the card assumed, and has a blast radius that made it wrong to carry as a rider.

The decision being honoured

#8287's ruling required an explicit choice on whether revocation/audit rows need the organization stamped. The decision taken: stamp from the row's own organization — the org the record is about — ⛔ not the actor's active organization. Rationale on that card: sys_api_key declares enable.trackHistory: true, and an audit row stamped with the wrong tenant is unreadable to the tenant admin it concerns under isolated, reproducing the very defect one layer down.

What the code actually does

packages/plugins/plugin-audit/src/audit-writers.ts:

const recordOrgId: string | undefined =
(typeof (ctx.result as any)?.organization_id === 'string' && (ctx.result as any).organization_id) ||
(typeof before?.organization_id === 'string' && before.organization_id) ||
undefined;
const tenantId: string | undefined = sess.tenantId ?? recordOrgId;

Two separate gaps:

  1. Precedence is the wrong way round for the decision.sess.tenantId ?? recordOrgId prefers the actor's active organization; the record's own organization is only a fallback. So a revocation performed from a session whose active org differs from the key's org stamps the actor's org — exactly the case the decision names. The existing comment explains the fallback was added so audit rows would not be invisible under RLS; the same reasoning argues for the record's org winning whenever it is present.
  2. The record-side read is spelled organization_id only.[finding] API keys carry no organization — under the isolated posture a minted key reads no org data at all (no leak, but the key surface is inert) #8287 gives sys_api_key an active_organization_id column (named that way deliberately — see the PR — so the credential table does not become org-walled and start hiding keys from their own owners). recordOrgId therefore resolves to undefined on this object and the fallback cannot fire even when precedence is fixed.

Why it was not fixed in #8287's PR

  • plugin-audit is a fifth lane, outside the four-lane surface the cross-lane designation (comment 5293093118) enumerated.
  • The precedence flip affects every audited object, not sys_api_key — that deserves its own measurement, not a rider on an identity card.
  • Landing only the narrow half (teaching recordOrgId about active_organization_id) would be inert without the precedence change, i.e. declared-but-unenforced — the shape AGENTS.md PD chore: version packages #10 / ADR-0049 forbid.

Scope for whoever takes this

  • Decide the precedence deliberately: is "the record's organization wins when present" correct for every audited object? It looks strictly more correct — an audit row describes a change to a record, and stamping the actor's org makes it unreadable to the tenant it concerns — but it is a behaviour change across the whole audit surface and should be measured, not assumed.
  • Decide how the writer learns which column carries a given object's organization. sys_api_key is the first object where the answer is not organization_id; a schema-level answer would be better than a second hard-coded name, and [finding] API keys carry no organization — under the isolated posture a minted key reads no org data at all (no leak, but the key surface is inert) #8287's PR deliberately did not invent one.
  • Repro: revoke another user's API key while your active organization differs from the key's, then read sys_audit_log as a tenant admin of the key's organization.

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions