RLSTesterSheet.tsx319 lines · main
1import {
2 acceptUntrustedSql,
3 safeSql,
4 type SafeSqlFragment,
5 type UntrustedSqlFragment,
6} from '@supabase/pg-meta'
7import {
8 Select,
9 SelectContent,
10 SelectGroup,
11 SelectItem,
12 SelectLabel,
13 SelectTrigger,
14 SelectValue,
15} from '@ui/components/shadcn/ui/select'
16import { LOCAL_STORAGE_KEYS, useFlag } from 'common'
17import { Code, ExternalLink } from 'lucide-react'
18import { useEffect, useRef, useState } from 'react'
19import {
20 Button,
21 DialogSectionSeparator,
22 Sheet,
23 SheetContent,
24 SheetDescription,
25 SheetFooter,
26 SheetHeader,
27 SheetSection,
28 SheetTitle,
29 SheetTrigger,
30} from 'ui'
31import { Admonition } from 'ui-patterns'
32
33import { InferredSQLViewer } from './InferredSQLViewer'
34import { type ParseQueryResults } from './RLSTester.types'
35import { RLSTesterEmptyState } from './RLSTesterEmptyState'
36import { RLSTesterResults } from './RLSTesterResults'
37import { RoleSelector } from './RoleSelector'
38import { SandboxManagement } from './SandboxManagement'
39import { UserSelector } from './UserSelector'
40import { UserSqlEditor } from './UserSqlEditor'
41import { useTestQueryRLS } from './useTestQueryRLS'
42import type { Policy } from '@/components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRow.utils'
43import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
44import { AiAssistantDropdown } from '@/components/ui/AiAssistantDropdown'
45import { FeaturePreviewBadge } from '@/components/ui/FeaturePreviewBadge'
46import { useTrack } from '@/lib/telemetry/track'
47import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state'
48import { PostgresSandboxProvider } from '@/state/postgres-sandbox/sandbox'
49import { useRoleImpersonationStateSnapshot } from '@/state/role-impersonation-state'
50import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
51
52interface RLSTesterSheetProps {
53 handleSelectEditPolicy: (policy: Policy) => void
54}
55
56export const RLSTesterSheet = (props: RLSTesterSheetProps) => {
57 return (
58 <PostgresSandboxProvider>
59 <RLSTesterSheetContents {...props} />
60 </PostgresSandboxProvider>
61 )
62}
63
64const RLSTesterSheetContents = ({ handleSelectEditPolicy }: RLSTesterSheetProps) => {
65 const track = useTrack()
66 const aiSnap = useAiAssistantStateSnapshot()
67 const { openSidebar } = useSidebarManagerSnapshot()
68 const { setRole } = useRoleImpersonationStateSnapshot()
69 const sandboxEnabled = useFlag('rlsTesterSandbox')
70
71 const [open, setOpen] = useState(false)
72 const [selectedOption, setSelectedOption] = useState<'anon' | 'authenticated'>('anon')
73
74 const [format, setFormat] = useState<'sql' | 'lib'>('sql')
75 const [inferredSQL, setInferredSQL] = useState<UntrustedSqlFragment>()
76
77 const [value, setValue] = useState<SafeSqlFragment>(safeSql``)
78 const [results, setResults] = useState<Object[] | null>(null)
79 const [autoLimit, setAutoLimit] = useState(false)
80 const [parseQueryResults, setParseQueryResults] = useState<ParseQueryResults>()
81
82 const {
83 testQuery,
84 inferSQLFromLib,
85 isLoading,
86 isInferring,
87 executeSqlError,
88 parseQueryError,
89 parseClientCodeError,
90 } = useTestQueryRLS()
91
92 const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
93
94 const handleValueChange = (sql: SafeSqlFragment) => {
95 setValue(sql)
96 if (format !== 'lib') return
97
98 if (debounceRef.current !== null) clearTimeout(debounceRef.current)
99 if (!sql) return
100
101 debounceRef.current = setTimeout(() => {
102 inferSQLFromLib(sql, setInferredSQL)
103 }, 1500)
104 }
105
106 const executionCallbacks = {
107 option: selectedOption,
108 onExecuteSQL: ({ result, isAutoLimit }: { result: Object[] | null; isAutoLimit: boolean }) => {
109 setResults(result)
110 setAutoLimit(isAutoLimit)
111 },
112 onParseQuery: setParseQueryResults,
113 }
114
115 const onRunQuery = async () => {
116 if (format === 'lib') {
117 if (!inferredSQL) return
118 await testQuery({ value: acceptUntrustedSql(inferredSQL), ...executionCallbacks })
119 track('rls_tester_run_query_clicked', { type: 'inferred' })
120 } else {
121 await testQuery({ value, ...executionCallbacks })
122 track('rls_tester_run_query_clicked', { type: 'raw' })
123 }
124 }
125
126 const assistantSql = format === 'lib' && inferredSQL ? acceptUntrustedSql(inferredSQL) : value
127
128 const getDebugPrompt = ({ includeSql = false }: { includeSql?: boolean } = {}) => {
129 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.`
130
131 return includeSql ? `${prompt}\n\nSQL Query:\n\`\`\`sql\n${assistantSql}\n\`\`\`` : prompt
132 }
133
134 const onDebugWithAssistant = () => {
135 const prompt = getDebugPrompt()
136 openSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
137 aiSnap.newChat({
138 name: 'Debug RLS policies',
139 sqlSnippets: [assistantSql],
140 initialInput: prompt,
141 })
142 setOpen(false)
143 }
144
145 useEffect(() => {
146 if (open) {
147 setRole({ type: 'postgrest', role: 'anon' })
148 } else {
149 // Flip back to service role
150 setRole(undefined)
151 }
152 }, [open, setRole])
153
154 return (
155 <Sheet open={open} onOpenChange={setOpen}>
156 <SheetTrigger asChild>
157 <Button type="default" icon={<Code />}>
158 Test
159 </Button>
160 </SheetTrigger>
161
162 <SheetContent className="w-[600px]! flex flex-col gap-y-0">
163 <SheetHeader>
164 <SheetTitle className="flex items-center gap-x-4">
165 <span>What data can my users see?</span>
166 <FeaturePreviewBadge featureKey={LOCAL_STORAGE_KEYS.UI_PREVIEW_RLS_TESTER} />
167 </SheetTitle>
168 <SheetDescription>
169 See what data a user is allowed to read based on your RLS policies
170 </SheetDescription>
171 </SheetHeader>
172
173 <div className="grow overflow-y-auto flex flex-col">
174 {sandboxEnabled && <SandboxManagement />}
175
176 <SheetSection className="px-0 py-0 border-t">
177 <div className="flex flex-col p-5 pt-4 gap-y-4">
178 <RoleSelector onSelectRole={setSelectedOption} />
179 {selectedOption === 'authenticated' && <UserSelector />}
180 </div>
181
182 <DialogSectionSeparator />
183
184 <div className="flex items-center justify-between px-5 py-2">
185 <p className="text-sm">Query</p>
186 <div className="flex items-center gap-x-2">
187 <Select
188 value={format}
189 onValueChange={(x) => {
190 const newFormat = x as 'sql' | 'lib'
191 setFormat(newFormat)
192 if (newFormat !== 'lib') {
193 setInferredSQL(undefined)
194 if (debounceRef.current !== null) clearTimeout(debounceRef.current)
195 }
196 }}
197 >
198 <SelectTrigger size="tiny">
199 <SelectValue />
200 </SelectTrigger>
201 <SelectContent>
202 <SelectGroup>
203 <SelectLabel>Query format</SelectLabel>
204 <SelectItem value="sql">SQL</SelectItem>
205 <SelectItem value="lib">Client library</SelectItem>
206 </SelectGroup>
207 </SelectContent>
208 </Select>
209 </div>
210 </div>
211
212 <div className="h-40 relative">
213 <UserSqlEditor
214 id="rls-tester"
215 value={value}
216 placeholder={
217 format === 'sql'
218 ? safeSql`select * from table;`
219 : safeSql`SQL will be inferred from client library code`
220 }
221 onChange={handleValueChange}
222 actions={{
223 runQuery: {
224 enabled: open,
225 callback: () => {
226 if (!isInferring && !isLoading) onRunQuery()
227 },
228 },
229 }}
230 />
231 </div>
232 </SheetSection>
233
234 {format === 'lib' && (
235 <div>
236 <DialogSectionSeparator />
237 <InferredSQLViewer sql={inferredSQL} isLoading={isInferring} />
238 </div>
239 )}
240
241 <DialogSectionSeparator />
242
243 {parseQueryError ? (
244 <div className="p-4">
245 <Admonition
246 type="warning"
247 title="Error parsing query"
248 description={parseQueryError.message}
249 />
250 </div>
251 ) : parseClientCodeError ? (
252 <div className="p-4">
253 <Admonition
254 type="warning"
255 title="Error parsing client code"
256 description={parseClientCodeError.message}
257 />
258 </div>
259 ) : (
260 executeSqlError && (
261 <div className="p-4">
262 <Admonition
263 type="warning"
264 title="Error running SQL query"
265 description={executeSqlError.message}
266 actions={[
267 <AiAssistantDropdown
268 key="ai-assistant"
269 label="Ask Assistant"
270 telemetrySource="rls_tester"
271 buildPrompt={() => getDebugPrompt({ includeSql: true })}
272 onOpenAssistant={onDebugWithAssistant}
273 />,
274 ]}
275 />
276 </div>
277 )
278 )}
279
280 {results === null ? (
281 !parseQueryError && !parseClientCodeError && !executeSqlError && <RLSTesterEmptyState />
282 ) : !!parseQueryResults ? (
283 <RLSTesterResults
284 results={results}
285 parseQueryResults={parseQueryResults}
286 autoLimit={autoLimit}
287 handleSelectEditPolicy={handleSelectEditPolicy}
288 />
289 ) : null}
290 </div>
291
292 <SheetFooter className="sm:justify-between">
293 <Button asChild type="default" icon={<ExternalLink />}>
294 <a
295 target="_blank"
296 rel="noopener noreferrer"
297 href="https://github.com/orgs/briven/discussions/45233"
298 >
299 Give feedback
300 </a>
301 </Button>
302 <div className="flex items-center gap-x-2">
303 <Button type="default" disabled={isLoading} onClick={() => setOpen(false)}>
304 Cancel
305 </Button>
306 <Button
307 type="primary"
308 loading={isInferring || isLoading}
309 disabled={format === 'lib' && !inferredSQL}
310 onClick={onRunQuery}
311 >
312 Run query
313 </Button>
314 </div>
315 </SheetFooter>
316 </SheetContent>
317 </Sheet>
318 )
319}