mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-10-21 01:22:54 +00:00
🗼 ci: Guard Scoped Model Applies the Isolation Plugin (#13458)
Adds a coverage test that enumerates all registered models and asserts every
schema with a tenantId field has the tenant-isolation plugin applied (detected
via the global Symbol.for('librechat:tenantIsolation') marker the plugin sets),
with a single documented allowlist entry — SystemGrant, which scopes tenancy
manually. A future model that ships a tenantId field without the plugin (the
exact gap that lets data leak across tenants) now fails CI instead of shipping
silently.
This commit is contained in:
parent
8120fc69cd
commit
547b28455a
1 changed files with 52 additions and 0 deletions
|
|
@ -0,0 +1,52 @@
|
|||
import mongoose from 'mongoose';
|
||||
import { createModels } from '../index';
|
||||
|
||||
/**
|
||||
* Global symbol set by applyTenantIsolation() on every schema it processes.
|
||||
* Recreated here via Symbol.for (the global registry) so the guard can detect
|
||||
* plugin application without the plugin exporting anything.
|
||||
*/
|
||||
const TENANT_ISOLATION_APPLIED = Symbol.for('librechat:tenantIsolation');
|
||||
|
||||
/**
|
||||
* Models that carry a `tenantId` field but intentionally do NOT use the
|
||||
* tenant-isolation plugin. SystemGrant scopes tenancy manually inside its
|
||||
* methods (see models/systemGrant). Adding an entry here must be a deliberate,
|
||||
* reviewed decision — that is the whole point of this guard.
|
||||
*/
|
||||
const MANUAL_TENANT_SCOPING = new Set<string>(['SystemGrant']);
|
||||
|
||||
function isPluginApplied(schema: mongoose.Schema): boolean {
|
||||
return (schema as unknown as { [key: symbol]: boolean })[TENANT_ISOLATION_APPLIED] === true;
|
||||
}
|
||||
|
||||
describe('tenant-isolation plugin coverage', () => {
|
||||
beforeAll(() => {
|
||||
createModels(mongoose);
|
||||
});
|
||||
|
||||
it('applies the tenant-isolation plugin to every model that has a tenantId field', () => {
|
||||
const missing: string[] = [];
|
||||
|
||||
for (const [name, model] of Object.entries(mongoose.models)) {
|
||||
const hasTenantId = Boolean(model.schema.path('tenantId'));
|
||||
if (!hasTenantId || MANUAL_TENANT_SCOPING.has(name)) {
|
||||
continue;
|
||||
}
|
||||
if (!isPluginApplied(model.schema)) {
|
||||
missing.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
expect(missing).toEqual([]);
|
||||
});
|
||||
|
||||
it('keeps the manual-scoping allowlist accurate (tenantId field, no plugin)', () => {
|
||||
for (const name of MANUAL_TENANT_SCOPING) {
|
||||
const model = mongoose.models[name];
|
||||
expect(model).toBeDefined();
|
||||
expect(Boolean(model.schema.path('tenantId'))).toBe(true);
|
||||
expect(isPluginApplied(model.schema)).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue