🛠️ fix: Normalize blank tenantId before persisting audit entries

The `recordAuditEntry` write path was treating any non-null tenantId as a
real string, so empty or whitespace-only values reached the schema validator,
failed the non-empty-string check, and silently dropped the audit row. The
read-side `tenantFilter` already treats those values as platform-level scope,
so the write path now mirrors it: blank or whitespace-only tenantId becomes
an omitted field, which matches `{ tenantId: { $exists: false } }` queries
and clears validation. Added a regression test that records two entries with
blank and whitespace tenantId and asserts both persist with the tenantId
field absent.
This commit is contained in:
Dustin Healy 2026-06-01 21:42:01 -07:00 • committed by Danny Avila
parent 2bbcafecd7
commit db0d5fda49
2 changed files with 23 additions and 1 deletions

View file

@ -69,6 +69,18 @@ describe('auditLog methods', () => {
const raw = await AuditLog.collection.findOne({});
expect(raw && Object.prototype.hasOwnProperty.call(raw, 'tenantId')).toBe(false);
});
it('treats blank and whitespace-only tenantId as platform-level (no silent drop)', async () => {
const blank = await methods.recordAuditEntry(baseInput({ tenantId: '' }));
const whitespace = await methods.recordAuditEntry(baseInput({ tenantId: ' ' }));
expect(blank).not.toBeNull();
expect(whitespace).not.toBeNull();
const rows = await AuditLog.collection.find({}).toArray();
expect(rows).toHaveLength(2);
for (const row of rows) {
expect(Object.prototype.hasOwnProperty.call(row, 'tenantId')).toBe(false);
}
});
});
describe('listAuditLogPage', () => {

View file

@ -120,6 +120,16 @@ function clampLimit(limit?: number): number {
export function createAuditLogMethods(mongoose: typeof import('mongoose')): AuditLogMethods {
async function recordAuditEntry(input: RecordAuditEntryInput): Promise<IAuditLog | null> {
const AuditLog = mongoose.models.AuditLog as Model<IAuditLog>;
/**
* Match the read-side `tenantFilter` convention: blank or whitespace-only
* tenant IDs are platform-level scope and the field must be omitted
* entirely (so the row matches `{ tenantId: { $exists: false } }` queries
* and satisfies the non-empty-string validator on the schema).
*/
const normalizedTenantId =
typeof input.tenantId === 'string' && input.tenantId.trim().length > 0
? input.tenantId
: undefined;
try {
const doc = await AuditLog.create({
action: input.action,
@ -129,7 +139,7 @@ export function createAuditLogMethods(mongoose: typeof import('mongoose')): Audi
targetPrincipalId: input.targetPrincipalId,
targetName: input.targetName,
capability: input.capability,
...(input.tenantId != null && { tenantId: input.tenantId }),
...(normalizedTenantId !== undefined && { tenantId: normalizedTenantId }),
});
return doc;
} catch (err) {