feat: add rls readiness gates

This commit is contained in:
Codex
2026-06-30 00:56:26 +08:00
parent 889a53c90f
commit f0cbc0b35a
2 changed files with 83 additions and 1 deletions

View File

@@ -141,6 +141,10 @@ function safeProviderName(provider) {
return String(provider || '').replace(/[^a-zA-Z0-9_.:-]/g, '_');
}
function formatTableName(row) {
return `${row.table_schema}.${row.table_name}`;
}
function validateEnv() {
const nodeEnv = env('NODE_ENV', 'development');
if (nodeEnv !== 'production') block('env.node_env', 'NODE_ENV must be production for production readiness checks');
@@ -418,6 +422,82 @@ async function validateDatabase() {
const unverifiedDomains = Number(unverifiedDomainRows.rows[0]?.count || 0);
if (unverifiedDomains > 0) warn('db.tenant_domains', 'Some tenant domains are not active/verified', { count: unverifiedDomains });
else pass('db.tenant_domains', 'Tenant domains are active/verified or not configured');
const tenantTablesWithoutRls = await pool.query(`
select c.table_schema, c.table_name
from information_schema.columns c
join pg_class cls on cls.relname = c.table_name
join pg_namespace ns on ns.oid = cls.relnamespace and ns.nspname = c.table_schema
where c.column_name = 'tenant_id'
and c.table_schema in ('public', 'app_private')
and cls.relkind in ('r', 'p')
and not cls.relrowsecurity
order by c.table_schema, c.table_name
`);
if (tenantTablesWithoutRls.rowCount > 0) {
block('db.rls.tenant_tables_enabled', 'Every tenant-scoped table must have row level security enabled', {
count: tenantTablesWithoutRls.rowCount,
samples: tenantTablesWithoutRls.rows.slice(0, 10).map(formatTableName),
});
} else {
pass('db.rls.tenant_tables_enabled', 'All tenant-scoped tables have RLS enabled');
}
const tenantTablesWithoutPolicies = await pool.query(`
select c.table_schema, c.table_name
from information_schema.columns c
join pg_class cls on cls.relname = c.table_name
join pg_namespace ns on ns.oid = cls.relnamespace and ns.nspname = c.table_schema
where c.column_name = 'tenant_id'
and c.table_schema in ('public', 'app_private')
and cls.relkind in ('r', 'p')
and not exists (
select 1
from pg_policy p
where p.polrelid = cls.oid
)
order by c.table_schema, c.table_name
`);
if (tenantTablesWithoutPolicies.rowCount > 0) {
block('db.rls.tenant_tables_policy', 'Every tenant-scoped table must have at least one RLS policy', {
count: tenantTablesWithoutPolicies.rowCount,
samples: tenantTablesWithoutPolicies.rows.slice(0, 10).map(formatTableName),
});
} else {
pass('db.rls.tenant_tables_policy', 'All tenant-scoped tables have at least one RLS policy');
}
const publicTenantTablesWithoutTenantContextPolicy = await pool.query(`
select c.table_schema, c.table_name
from information_schema.columns c
join pg_class cls on cls.relname = c.table_name
join pg_namespace ns on ns.oid = cls.relnamespace and ns.nspname = c.table_schema
where c.column_name = 'tenant_id'
and c.table_schema = 'public'
and cls.relkind in ('r', 'p')
and not exists (
select 1
from pg_policy p
where p.polrelid = cls.oid
and (
lower(pg_get_expr(p.polqual, p.polrelid)) like '%app.current_tenant_id()%'
or lower(pg_get_expr(p.polwithcheck, p.polrelid)) like '%app.current_tenant_id()%'
)
)
order by c.table_schema, c.table_name
`);
if (publicTenantTablesWithoutTenantContextPolicy.rowCount > 0) {
block(
'db.rls.public_tenant_context',
'Every public tenant-scoped table must include app.current_tenant_id() in an RLS policy',
{
count: publicTenantTablesWithoutTenantContextPolicy.rowCount,
samples: publicTenantTablesWithoutTenantContextPolicy.rows.slice(0, 10).map(formatTableName),
},
);
} else {
pass('db.rls.public_tenant_context', 'Public tenant-scoped tables include tenant context in RLS policies');
}
} finally {
await pool.end();
}