Skip to content

Commit 576bbe5

Browse files
hotlongCopilot
andcommitted
fix(plugin-audit): inherit organization_id from audited record when session lacks tenantId
Audit rows were being written with organization_id=NULL whenever the session's activeOrganizationId wasn't populated (background jobs, first requests post-signin, sudo paths). Combined with the SecurityPlugin's RLS predicate that gates reads on organization_id matching the current user's tenant, this made the audit log UI appear permanently empty even though writes succeeded — historical /api/v1/data/sys_audit_log queries returned 0 rows despite the underlying table holding thousands of entries. Falls back to the audited record's own organization_id (from after for insert/update, before for delete), so the audit row is always at least as scoped as the record it describes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent d9c290a commit 576bbe5

1 file changed

Lines changed: 18 additions & 5 deletions

File tree

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

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -155,13 +155,12 @@ export function installAuditWriters(engine: any, packageId = 'com.objectstack.au
155155
* audit failures.
156156
*/
157157
const writeAudit = async (ctx: HookContext) => {
158-
process.stderr.write(`[AuditWriter] hook event=${ctx.event} object=${ctx.object} hasApi=${!!(ctx as any).api} hasSudo=${!!(ctx as any).api?.sudo}\n`);
159158
if (SKIP_OBJECTS.has(ctx.object)) return;
160159
const action = actionFor(ctx.event);
161160
if (!action) return;
162161

163162
const api: any = (ctx as any).api;
164-
if (!api?.sudo) { process.stderr.write(`[AuditWriter] BAIL no api.sudo for ${ctx.object}\n`); return; }
163+
if (!api?.sudo) return;
165164

166165
const after: any = ctx.result;
167166
const before: any = (ctx as any).__previous ?? (ctx as any).previous ?? null;
@@ -175,7 +174,23 @@ export function installAuditWriters(engine: any, packageId = 'com.objectstack.au
175174

176175
const sess: any = (ctx as any).session ?? {};
177176
const userId: string | undefined = sess.userId;
178-
const tenantId: string | undefined = sess.tenantId;
177+
// Prefer the active session tenant, but fall back to the audited
178+
// record's own `organization_id`. This matters in two cases:
179+
// 1. Background jobs / unauthenticated sudo paths where the
180+
// session has no `tenantId` populated.
181+
// 2. better-auth's `activeOrganizationId` cache miss on first
182+
// requests after sign-in, before the active-org has been set
183+
// on the session row.
184+
// Without this fallback, audit rows are written with
185+
// `organization_id=NULL` and the SecurityPlugin's RLS predicate
186+
// (`organization_id = current_user.organization_id`) hides them
187+
// forever — making the audit log UI appear permanently empty even
188+
// though writes succeed.
189+
const recordOrgId: string | undefined =
190+
(typeof (ctx.result as any)?.organization_id === 'string' && (ctx.result as any).organization_id) ||
191+
(typeof ((ctx as any).__previous as any)?.organization_id === 'string' && ((ctx as any).__previous as any).organization_id) ||
192+
undefined;
193+
const tenantId: string | undefined = sess.tenantId ?? recordOrgId;
179194

180195
let oldValue: Record<string, any> | null = null;
181196
let newValue: Record<string, any> | null = null;
@@ -236,7 +251,6 @@ export function installAuditWriters(engine: any, packageId = 'com.objectstack.au
236251
try {
237252
const sys = api.sudo();
238253
await sys.object('sys_audit_log').create(auditRow);
239-
process.stderr.write(`[AuditWriter] WROTE audit_log object=${ctx.object} action=${action}\n`);
240254
await sys.object('sys_activity').create(activityRow);
241255
// M10.8: write per-user inbox notifications. Best-effort; never
242256
// throws into the user-facing CRUD path. Covers two common cases:
@@ -259,7 +273,6 @@ export function installAuditWriters(engine: any, packageId = 'com.objectstack.au
259273
tenantId: tenantId ?? null,
260274
});
261275
} catch (err) {
262-
process.stderr.write(`[AuditWriter] WRITE FAILED: ${String((err as any)?.message ?? err)}\n`);
263276
// Log via engine logger if available, but never throw.
264277
try { (engine as any).logger?.warn?.('Audit write failed', { object: ctx.object, action, err: String((err as any)?.message ?? err) }); } catch {}
265278
}

0 commit comments

Comments
 (0)