mirror of
https://github.com/supabase/supabase.git
synced 2026-05-07 09:20:21 -04:00
5f867e5f6c
## Context Resolves FE-3077 Related discussion: https://github.com/orgs/supabase/discussions/45233 Verifying the correctness of your RLS policies set up has always been a gap, as highlighted by a number of GitHub discussions like [here](https://github.com/orgs/supabase/discussions/12269) and [here](https://github.com/orgs/supabase/discussions/14401). As such, we're piloting a dedicated UI for RLS testing (using role impersonation as the base), in which you'll be able to - Run a SQL query as a user (not logged in / logged in - this is the role impersonation part) - See which RLS policies are being evaluated as part of the query - And hopefully be able to debug which policies are not set up correctly Changes are currently set as a feature preview - and we'll iterate as we get feedback from everyone 🙂 🙏 <img width="613" height="957" alt="image" src="https://github.com/user-attachments/assets/83c37f8a-28fc-43b3-b0ff-e28571d8710c" /> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * RLS Tester: run queries as anon or authenticated users, view inferred SQL, per-table policy summaries, and data previews of accessible rows. * UI preview: new RLS Tester preview card and modal with opt-in toggle; RLS Tester sheet with role/user selector and query editor. * SQLEditor: “Explain” tab is always visible. * **Chores** * Added supporting API endpoints, background checks for table RLS status, and a local-storage flag to persist the preview opt-in. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
66 lines
2.1 KiB
TypeScript
66 lines
2.1 KiB
TypeScript
import { parse } from 'libpg-query'
|
|
import { NextApiRequest, NextApiResponse } from 'next'
|
|
|
|
const getOperation = async (sql: string) => {
|
|
const ast = await parse(sql)
|
|
const stmt = ast.stmts?.[0]?.stmt
|
|
|
|
if (!stmt) return null
|
|
|
|
if ('SelectStmt' in stmt) return 'SELECT'
|
|
if ('InsertStmt' in stmt) return 'INSERT'
|
|
if ('UpdateStmt' in stmt) return 'UPDATE'
|
|
if ('DeleteStmt' in stmt) return 'DELETE'
|
|
}
|
|
|
|
const getTablesInQuery = async (sql: string) => {
|
|
const ast = await parse(sql)
|
|
const tables: string[] = []
|
|
|
|
function traverse(node: unknown): void {
|
|
if (!node || typeof node !== 'object') return
|
|
const obj = node as Record<string, unknown>
|
|
|
|
if ('RangeVar' in obj) {
|
|
const rv = obj.RangeVar as { relname?: string; schemaname?: string }
|
|
if (rv.relname) tables.push(rv.schemaname ? `${rv.schemaname}.${rv.relname}` : rv.relname)
|
|
}
|
|
|
|
if ('relation' in obj && obj.relation && typeof obj.relation === 'object') {
|
|
const rv = obj.relation as { relname?: string; schemaname?: string }
|
|
if (rv.relname) tables.push(rv.schemaname ? `${rv.schemaname}.${rv.relname}` : rv.relname)
|
|
}
|
|
|
|
for (const value of Object.values(obj)) {
|
|
Array.isArray(value) ? value.forEach(traverse) : traverse(value)
|
|
}
|
|
}
|
|
|
|
traverse(ast)
|
|
return [...new Set(tables)].sort((a, b) => a.localeCompare(b))
|
|
}
|
|
|
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|
if (req.method !== 'POST') {
|
|
res.setHeader('Allow', ['POST'])
|
|
return res.status(405).json({ error: `Method ${req.method} Not Allowed` })
|
|
}
|
|
|
|
try {
|
|
const { sql } = req.body
|
|
|
|
if (typeof sql !== 'string' || sql.trim().length === 0) {
|
|
return res.status(400).json({ error: 'Missing or invalid "sql" in request body' })
|
|
}
|
|
|
|
const tables = await getTablesInQuery(sql)
|
|
const operation = await getOperation(sql)
|
|
return res.status(200).json({ tables, operation })
|
|
} catch (error) {
|
|
const message =
|
|
(error as { sqlDetails?: { message?: string } })?.sqlDetails?.message ??
|
|
(error instanceof Error ? error.message : 'Failed to parse SQL')
|
|
return res.status(400).json({ error: message })
|
|
}
|
|
}
|