import { acceptUntrustedSql, safeSql, type SafeSqlFragment, type UntrustedSqlFragment, } from '@supabase/pg-meta' import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue, } from '@ui/components/shadcn/ui/select' import { LOCAL_STORAGE_KEYS, useFlag } from 'common' import { Code, ExternalLink } from 'lucide-react' import { useEffect, useRef, useState } from 'react' import { Button, DialogSectionSeparator, Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetSection, SheetTitle, SheetTrigger, } from 'ui' import { Admonition } from 'ui-patterns/admonition' import { ConfirmationModal } from 'ui-patterns/Dialogs/ConfirmationModal' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { InferredSQLViewer } from './InferredSQLViewer' import { type ParseQueryResults } from './RLSTester.types' import { RLSTesterEmptyState } from './RLSTesterEmptyState' import { RLSTesterResults } from './RLSTesterResults' import { RoleSelector } from './RoleSelector' import { SandboxManagement } from './SandboxManagement' import { UserSelector } from './UserSelector' import { UserSqlEditor } from './UserSqlEditor' import { useTestQueryRLS, type TestQueryBlockedReason } from './useTestQueryRLS' import type { Policy } from '@/components/interfaces/Database/Policies/PolicyTableRow/PolicyTableRow.utils' import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider' import { AiAssistantDropdown } from '@/components/ui/AiAssistantDropdown' import { FeaturePreviewBadge } from '@/components/ui/FeaturePreviewBadge' import { useTrack } from '@/lib/telemetry/track' import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state' import { PostgresSandboxProvider, usePostgresSandbox } from '@/state/postgres-sandbox/sandbox' import { useRoleImpersonationStateSnapshot } from '@/state/role-impersonation-state' import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state' interface RLSTesterSheetProps { handleSelectEditPolicy: (policy: Policy) => void } export const RLSTesterSheet = (props: RLSTesterSheetProps) => { return ( ) } const RLSTesterSheetContents = ({ handleSelectEditPolicy }: RLSTesterSheetProps) => { const track = useTrack() const aiSnap = useAiAssistantStateSnapshot() const { openSidebar } = useSidebarManagerSnapshot() const { setRole } = useRoleImpersonationStateSnapshot() const { startSandbox, status, isSyncing } = usePostgresSandbox() const sandboxEnabled = useFlag('rlsTesterSandbox') const sandboxIsStarting = status === 'loading' const [open, setOpen] = useState(false) const [selectedOption, setSelectedOption] = useState<'anon' | 'authenticated'>('anon') const [blockedReason, setBlockedReason] = useState() const [format, setFormat] = useState<'sql' | 'lib'>('sql') const [inferredSQL, setInferredSQL] = useState() const [value, setValue] = useState(safeSql``) const [results, setResults] = useState(null) const [autoLimit, setAutoLimit] = useState(false) const [parseQueryResults, setParseQueryResults] = useState() const { testQuery, inferSQLFromLib, isLoading, isInferring, executeSqlError, parseQueryError, parseClientCodeError, } = useTestQueryRLS() const isErrorDueToRLS = executeSqlError?.message.includes('violates row-level security policy') ?? false const mutationOperation = blockedReason?.type === 'mutation' ? blockedReason.operation : undefined const debounceRef = useRef | null>(null) const handleValueChange = (sql: SafeSqlFragment) => { setValue(sql) if (format !== 'lib') return if (debounceRef.current !== null) clearTimeout(debounceRef.current) if (!sql) return debounceRef.current = setTimeout(() => inferSQLFromLib(sql, setInferredSQL), 1500) } const executionCallbacks = { option: selectedOption, acknowledgeMutation: blockedReason?.type === 'mutation', onExecuteSQL: ({ result, isAutoLimit }: { result: Object[] | null; isAutoLimit: boolean }) => { setResults(result) setAutoLimit(isAutoLimit) }, onParseQuery: setParseQueryResults, onValidationBlocked: setBlockedReason, } const onRunQuery = async () => { setBlockedReason(undefined) if (format === 'lib') { if (!inferredSQL) return const blocked = await testQuery({ value: acceptUntrustedSql(inferredSQL), ...executionCallbacks, }) if (!blocked) track('rls_tester_run_query_clicked', { type: 'inferred' }) } else { const blocked = await testQuery({ value, ...executionCallbacks }) if (!blocked) track('rls_tester_run_query_clicked', { type: 'raw' }) } } const assistantSql = format === 'lib' && inferredSQL ? acceptUntrustedSql(inferredSQL) : value const getDebugPrompt = ({ includeSql = false }: { includeSql?: boolean } = {}) => { const prompt = `Help me fix my RLS policy based on the attached SQL snippet that gave the following error: \n\n${executeSqlError?.message}\n\nEvaluate if the problem might be query first, before checking my RLS policies.` return includeSql ? `${prompt}\n\nSQL Query:\n\`\`\`sql\n${assistantSql}\n\`\`\`` : prompt } const onDebugWithAssistant = () => { const prompt = getDebugPrompt() openSidebar(SIDEBAR_KEYS.AI_ASSISTANT) aiSnap.newChat({ name: 'Debug RLS policies', sqlSnippets: [assistantSql], initialInput: prompt, }) setOpen(false) } useEffect(() => { setRole({ type: 'postgrest', role: 'anon' }) return () => { // Flip back to service role setRole(undefined) } // [Joshen] Intentional - to only reset back to service role when navigating away // eslint-disable-next-line react-hooks/exhaustive-deps }, []) return ( <> What data can my users access? See what data a user is allowed to read or modify based on your RLS policies
{sandboxEnabled && }
{selectedOption === 'authenticated' && }

Query

{ if (!isInferring && !isLoading) onRunQuery() }, }, }} />
{format === 'lib' && (
)} {blockedReason?.type === 'multiple-statements' ? (
) : blockedReason?.type === 'unsupported-operation' ? (
) : parseQueryError ? (
) : parseClientCodeError ? (
) : executeSqlError && !isErrorDueToRLS ? (
getDebugPrompt({ includeSql: true })} onOpenAssistant={onDebugWithAssistant} />, ]} />
) : isLoading ? (
) : results === null && !isErrorDueToRLS ? ( ) : !!parseQueryResults ? ( ) : null}
setBlockedReason(undefined)} alert={{ title: `This ${mutationOperation} query will run against your actual database`, description: 'Your database may be directly modified as a result. Are you sure?', }} > {sandboxEnabled && ( <>

We highly recommend using the sandbox to set up an ephemeral database environment for testing insert, update, or delete queries.

)}
) }