RLSTesterSheet.tsx 11 KB

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