Files
supabase/apps/studio/components/interfaces/SQLEditor/RunQueryWarningModal.tsx
T
Alaister YoungandAlaister Young 3aed9a9a2a feat(studio): warn before CREATE TABLE without RLS in SQL editor (#45008)
Adds a pre-execution warning in the SQL editor when a `CREATE TABLE`
statement is run without enabling Row Level Security on the new table.
Responds to the press call-out around SQL editor security.

<img width="708" height="498" alt="Screenshot 2026-04-18 at 4 31 07 PM"
src="https://github.com/user-attachments/assets/4f23ed5e-f32c-46f0-b0da-ac6d4c661c7c"
/>


**Added:**
- Pre-execution check in `executeQuery` that detects `CREATE TABLE`
statements without a matching `ALTER TABLE ... ENABLE ROW LEVEL
SECURITY` in the same submitted SQL.
- New "Run and enable RLS" action in the warning modal that rewrites the
SQL to append `ALTER TABLE [schema.]<table> ENABLE ROW LEVEL SECURITY;`
for each detected table before running.
- Link in the modal to the RLS docs.

**Changed:**
- `RunQueryWarningModal` now renders `Dialog` directly (instead of
`ConfirmationModal`) so it can show three buttons: Cancel / Run without
RLS / Run and enable RLS.
- `sqlEventParser` table-name regex now supports quoted identifiers
containing spaces (e.g. `"My Table"`) and escaped quotes (e.g.
`"user""table"`).

The check runs against the SQL that's actually submitted, so
partial-selection works correctly — selecting only the `CREATE TABLE`
portion will trigger the warning even if there's a matching `ENABLE RLS`
lower in the editor.

## To test

- Open the SQL editor and run `create table foo (id int8 primary key);`
→ modal should appear with the RLS warning bullet and three buttons.
- Click **Run and enable RLS** → query runs, table is created with RLS
enabled.
- Click **Run without RLS** → query runs as written, no RLS.
- Run `create table foo (id int8); alter table foo enable row level
security;` → no modal (RLS already enabled in same submission).
- Run `create table public.bar (id int8); create table baz (id int8);
alter table baz enable rls;` → modal flags only `public.bar`.
- Select only the `create table` portion of a snippet that also enables
RLS lower down and run the selection → modal should still fire.
- Run an existing destructive query (`drop table x`) → modal still works
as before with two buttons (Cancel / Run this query).

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* SQL editor now detects CREATE TABLE statements missing Row Level
Security (RLS) and shows counts and dynamic table/schema details in a
redesigned warning dialog with updated pluralization and a “Learn more”
link.
* New actions: “Run without RLS” and, when available, “Run and enable
RLS” which applies RLS and runs the query; editor can execute an
overridden SQL payload when applying RLS changes.

* **Tests**
* Added comprehensive unit and e2e tests covering RLS detection, SQL
augmentation, trigger handling, identifier parsing, and the “Run and
enable RLS” flow.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
2026-04-18 17:18:12 +08:00

162 lines
5.8 KiB
TypeScript

import {
Button,
cn,
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogSection,
DialogSectionSeparator,
DialogTitle,
} from 'ui'
import { Admonition } from 'ui-patterns'
import { PotentialIssues } from './SQLEditor.types'
import { DOCS_URL } from '@/lib/constants'
interface RunQueryWarningModalProps {
visible: boolean
potentialIssues: PotentialIssues | undefined
onCancel: () => void
onConfirm: () => void
onConfirmWithRLS?: () => void
}
export const RunQueryWarningModal = ({
visible,
potentialIssues,
onCancel,
onConfirm,
onConfirmWithRLS,
}: RunQueryWarningModalProps) => {
const {
hasDestructiveOperations,
hasUpdateWithoutWhere,
hasAlterDatabasePreventConnection,
createTablesMissingRLS,
} = potentialIssues || {}
const missingRLSTables = createTablesMissingRLS ?? []
const hasMissingRLS = missingRLSTables.length > 0
const issueCount =
(hasDestructiveOperations ? 1 : 0) +
(hasUpdateWithoutWhere ? 1 : 0) +
(hasAlterDatabasePreventConnection ? 1 : 0) +
(hasMissingRLS ? 1 : 0)
return (
<Dialog
open={visible}
onOpenChange={(open) => {
if (!open) onCancel()
}}
>
<DialogContent aria-describedby={undefined} className="p-0 gap-0 pb-5 !block" size="large">
<DialogHeader className={cn('border-b')} padding="small">
<DialogTitle>
{`Potential issue${issueCount > 1 ? 's' : ''} detected with your query`}
</DialogTitle>
<DialogDescription className="sr-only">
Review the warnings below before running this query.
</DialogDescription>
</DialogHeader>
<Admonition
type="warning"
label={
issueCount > 1
? 'The following potential issues have been detected:'
: 'The following potential issue has been detected:'
}
description="Ensure that these are intentional before executing this query"
className="border-x-0 rounded-none -mt-px"
/>
<DialogSection padding="small">
<div className="text-sm">
<ul className="border rounded-md grid bg-surface-200 divide-y">
{hasDestructiveOperations && (
<li className="grid pt-3 pb-2 px-4">
<span className="font-bold">Query has destructive operations</span>
<span className="text-foreground-light">
Make sure you are not accidentally removing something important.
</span>
</li>
)}
{hasUpdateWithoutWhere && (
<li className="grid pt-2 pb-3 px-4 gap-1">
<span className="font-bold">Query uses update without a where clause</span>
<span className="text-foreground-light">
Without a <code className="text-code-inline">where</code> clause, this could
update all rows in the table.
</span>
</li>
)}
{hasAlterDatabasePreventConnection && (
<li className="grid pt-2 pb-3 px-4 gap-1">
<span className="font-bold">Query will prevent connections to your database</span>
<span className="text-foreground-light">
The dashboard will no longer have access to your database, and you will need a
direct connection to your database to reconfigure this setting
</span>
</li>
)}
{hasMissingRLS && (
<li className="grid pt-2 pb-3 px-4 gap-1">
<span className="font-bold">
{missingRLSTables.length === 1
? 'New table will not have Row Level Security enabled'
: 'New tables will not have Row Level Security enabled'}
</span>
<span className="text-foreground-light">
Without RLS, any client using your project's anon or authenticated keys can read
and write to{' '}
{missingRLSTables.length === 1 ? (
<code className="text-code-inline">
{missingRLSTables[0].schema
? `${missingRLSTables[0].schema}.${missingRLSTables[0].tableName}`
: missingRLSTables[0].tableName}
</code>
) : (
'these tables'
)}
. Enable RLS and add policies before exposing this table via the API.{' '}
<a
href={`${DOCS_URL}/guides/database/postgres/row-level-security`}
target="_blank"
rel="noreferrer"
className="underline"
>
Learn more
</a>
.
</span>
</li>
)}
</ul>
</div>
<p className="mt-4 text-sm text-foreground-light">
Please confirm that you would like to execute this query.
</p>
</DialogSection>
<DialogSectionSeparator />
<div className="flex flex-wrap gap-2 px-5 pt-5">
<Button size="medium" type="default" onClick={() => onCancel()}>
Cancel
</Button>
<Button size="medium" type="warning" onClick={onConfirm} className="ml-auto">
{hasMissingRLS ? 'Run without RLS' : 'Run this query'}
</Button>
{hasMissingRLS && onConfirmWithRLS && (
<Button size="medium" type="primary" onClick={onConfirmWithRLS}>
Run and enable RLS
</Button>
)}
</div>
</DialogContent>
</Dialog>
)
}