forked from wangziqi/gongxue-base
feat: add rls readiness gates
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user