Files
gongxue-base/scripts/data-api-security-contract-test.js
2026-07-12 19:26:57 +08:00

170 lines
6.7 KiB
JavaScript

import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
const repoRoot = process.cwd();
const migrationPath = path.join(
repoRoot,
'supabase',
'migrations',
'202607110001_data_api_acl_rls_hardening.sql',
);
const authBoundaryMigrationPath = path.join(
repoRoot,
'supabase',
'migrations',
'202607120018_auth_user_reference_boundary.sql',
);
const readinessPath = path.join(repoRoot, 'scripts', 'production-readiness-check.js');
const privilegedBootstrapPath = path.join(
repoRoot,
'scripts',
'deploy',
'sql',
'bootstrap-backend-runtime-roles.sql',
);
const packagePath = path.join(repoRoot, 'package.json');
const taroSourceRoot = path.join(repoRoot, 'apps', 'taro', 'src');
function read(filePath) {
return fs.readFileSync(filePath, 'utf8');
}
function walk(dir) {
return fs.readdirSync(dir, { withFileTypes: true }).flatMap(entry => {
const filePath = path.join(dir, entry.name);
return entry.isDirectory() ? walk(filePath) : [filePath];
});
}
const migration = read(migrationPath);
const authBoundaryMigration = read(authBoundaryMigrationPath);
const privilegedBootstrap = read(privilegedBootstrapPath);
assert.match(
privilegedBootstrap,
/revoke execute on all functions in schema public[\s\S]*from public, anon, authenticated, tiku_api, tiku_worker/i,
'the privileged bootstrap must close Supabase base-image extension RPC execution',
);
assert.match(
privilegedBootstrap,
/alter default privileges for role %I revoke execute on functions from public, anon, authenticated, tiku_api, tiku_worker/i,
'the privileged bootstrap must keep future extension-owner functions closed',
);
assert.match(
migration,
/revoke all privileges on all tables in schema public from public, anon, authenticated/i,
'existing public tables must not be exposed to client Data API roles',
);
assert.match(
authBoundaryMigration,
/revoke all on function app\.auth_user_exists\(uuid\)[\s\S]*from public, anon, authenticated, service_role, tiku_api, tiku_worker/i,
'the Auth existence boundary must be denied to every Data API role before the API-only grant',
);
assert.doesNotMatch(
authBoundaryMigration,
/grant execute on function app\.auth_user_exists\(uuid\) to (?:anon|authenticated|service_role)/i,
'the Auth existence boundary must never be exposed through PostgREST roles',
);
assert.match(
migration,
/revoke all privileges on all sequences in schema public from public, anon, authenticated/i,
'existing public sequences must not be exposed to client Data API roles',
);
assert.match(
migration,
/revoke all privileges on all functions in schema public from public, anon, authenticated/i,
'existing public RPC functions must not be exposed to client Data API roles',
);
assert.match(
migration,
/public functions remain executable by anon\/authenticated; run the privileged backend runtime role bootstrap before migrations/i,
'normal migrations must fail closed when Supabase-owned extension functions remain exposed',
);
assert.match(
migration,
/alter default privileges for role %I in schema public revoke all privileges on tables from public, anon, authenticated/i,
'future public tables must default to no client Data API grant',
);
assert.match(
migration,
/alter default privileges for role %I in schema public revoke all privileges on sequences from public, anon, authenticated/i,
'future public sequences must default to no client Data API grant',
);
assert.match(
migration,
/revoke create on schema public from public, anon, authenticated/i,
'client roles must not create objects in the exposed public schema',
);
assert.match(
migration,
/alter default privileges for role %I revoke execute on functions from public, anon, authenticated/i,
'future public RPC functions must require an explicit execute grant',
);
assert.match(
migration,
/select distinct owner_role\.rolname[\s\S]*pg_has_role\(current_user, owner_role\.oid, 'MEMBER'\)/i,
'default privileges must cover every public owner the migration role is authorized to manage',
);
assert.match(migration, /drop policy if exists platform_admin_platform_users/i);
assert.match(
migration,
/create policy platform_users_self_read[\s\S]*for select[\s\S]*to authenticated[\s\S]*auth_user_id\s*=\s*\(select auth\.uid\(\)\)/i,
'platform_users may expose only an explicit self-read policy to authenticated users',
);
assert.doesNotMatch(
migration,
/create policy [^;]+ on public\.platform_users[\s\S]*?for\s+(?:all|insert|update|delete)/i,
'platform_users must not have a client write policy',
);
assert.match(
migration,
/create or replace function app\.is_platform_admin\(\)[\s\S]*security definer[\s\S]*set search_path = ''[\s\S]*from public\.platform_users[\s\S]*status = 'active'/i,
'RLS platform authority must come from an active database identity',
);
assert.doesNotMatch(
migration,
/select\s+app\.current_role\(\)\s+in\s*\([^)]*platform_admin/i,
'a platform_admin JWT role claim must not be sufficient for RLS authority',
);
const readiness = read(readinessPath);
for (const gateId of [
'db.data_api.public_table_acl',
'db.data_api.public_sequence_acl',
'db.data_api.public_function_acl',
'db.data_api.public_default_acl',
'db.data_api.platform_users_write_policy',
'db.rls.platform_admin_authority',
]) {
assert.ok(readiness.includes(gateId), `production database readiness must include ${gateId}`);
}
const sdkImportViolations = [];
const dataApiCallViolations = [];
for (const filePath of walk(taroSourceRoot).filter(file => /\.(?:ts|tsx)$/.test(file))) {
const source = read(filePath);
const relative = path.relative(repoRoot, filePath).replace(/\\/g, '/');
if (source.includes('@supabase/supabase-js') && relative !== 'apps/taro/src/services/supabase.ts') {
sdkImportViolations.push(relative);
}
if (!source.includes('ensureSupabaseClient') && !source.includes('getSupabaseClient') && !source.includes('@supabase/supabase-js')) {
continue;
}
for (const match of source.matchAll(/\b([A-Za-z_$][\w$]*)\.(from|rpc)\s*\(/g)) {
if (match[1] !== 'Array' && match[1] !== 'Buffer') {
dataApiCallViolations.push(`${relative}:${match[0]}`);
}
}
}
assert.deepEqual(sdkImportViolations, [], 'Supabase SDK ownership must stay centralized in services/supabase.ts');
assert.deepEqual(dataApiCallViolations, [], 'Taro must not access business tables or RPCs through the Data API');
const rootPackage = JSON.parse(read(packagePath));
assert.ok(rootPackage.scripts?.['test:data-api:security'], 'root package must expose the Data API security contract test');
assert.ok(
rootPackage.scripts?.['test:readiness']?.includes('data-api-security-contract-test.js'),
'the production readiness contract suite must run the Data API security test',
);
console.log('[PASS] Supabase Data API deny-by-default security contract');