useTestQueryRLS.ts 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. import { type SafeSqlFragment, type UntrustedSqlFragment } from '@supabase/pg-meta'
  2. import { useState } from 'react'
  3. import { toast } from 'sonner'
  4. import { checkIfAppendLimitRequired, suffixWithLimit } from '../../SQLEditor/SQLEditor.utils'
  5. import { type ParseQueryResults } from './RLSTester.types'
  6. import { filterTablePolicies } from './useTestQueryRLS.utils'
  7. import { useParseClientCodeMutation } from '@/data/ai/parse-client-code-mutation'
  8. import { useDatabasePoliciesQuery } from '@/data/database-policies/database-policies-query'
  9. import { useCheckTableRLSStatusMutation } from '@/data/database/table-check-rls-mutation'
  10. import { useParseSQLQueryMutation } from '@/data/misc/parse-query-mutation'
  11. import { useExecuteSqlMutation } from '@/data/sql/execute-sql-mutation'
  12. import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
  13. import { wrapWithRoleImpersonation } from '@/lib/role-impersonation'
  14. import { usePostgresSandbox } from '@/state/postgres-sandbox/sandbox'
  15. import {
  16. isRoleImpersonationEnabled,
  17. useGetImpersonatedRoleState,
  18. useImpersonatedUser,
  19. useRoleImpersonationStateSnapshot,
  20. } from '@/state/role-impersonation-state'
  21. const limit = 100
  22. /**
  23. * [Joshen] Testing a SQL query for its RLS access involves 3 async steps
  24. * 0. (Optional) Inferring client library code to SQL query via the AI Assistant
  25. * 1. Parsing the provided SQL query to retrieve its operation type + tables involved
  26. * 2. Checking for tables involved if they've got RLS enabled
  27. * 3. Actually running the query to retrieve the results
  28. *
  29. * Errors should all be handled as part of the UI instead of toasts, hence the empty onError
  30. * handlers to mute the default error handlers within the react query mutationhooks
  31. */
  32. export const useTestQueryRLS = () => {
  33. const { data: project } = useSelectedProjectQuery()
  34. const { role } = useRoleImpersonationStateSnapshot()
  35. const { sandbox } = usePostgresSandbox()
  36. const getImpersonatedRoleState = useGetImpersonatedRoleState()
  37. const impersonatedRoleState = getImpersonatedRoleState()
  38. const user = useImpersonatedUser()
  39. const [isLoading, setIsLoading] = useState(false)
  40. const [sandboxError, setSandboxError] = useState<Error>()
  41. const { data: policies = [] } = useDatabasePoliciesQuery({
  42. projectRef: project?.ref,
  43. connectionString: project?.connectionString,
  44. })
  45. const { mutateAsync: executeSql, error: executeSqlMutationError } = useExecuteSqlMutation({
  46. onError: () => {},
  47. })
  48. const executeSqlError = sandbox ? sandboxError : executeSqlMutationError
  49. const {
  50. mutateAsync: parseClientCode,
  51. isPending: isInferring,
  52. error: parseClientCodeError,
  53. } = useParseClientCodeMutation({
  54. onError: () => {},
  55. })
  56. const inferSQLFromLib = async (
  57. value: string,
  58. onInferSQL: (unchecked_sql: UntrustedSqlFragment) => void
  59. ) => {
  60. const { unchecked_sql, valid } = await parseClientCode({ code: value })
  61. if (valid && unchecked_sql != null) {
  62. onInferSQL(unchecked_sql)
  63. } else {
  64. toast.error('Client library code provided is not valid')
  65. }
  66. }
  67. const { mutateAsync: parseQuery, error: parseQueryError } = useParseSQLQueryMutation({
  68. onError: () => {},
  69. })
  70. const { mutateAsync: getTableRLSStatus, error: getTableRLSStatusError } =
  71. useCheckTableRLSStatusMutation({
  72. onError: () => {},
  73. })
  74. const testQuery = async ({
  75. value,
  76. option,
  77. onExecuteSQL,
  78. onParseQuery,
  79. }: {
  80. value: SafeSqlFragment
  81. option: 'anon' | 'authenticated'
  82. onExecuteSQL: ({
  83. result,
  84. isAutoLimit,
  85. }: {
  86. result: Object[] | null
  87. isAutoLimit: boolean
  88. }) => void
  89. onParseQuery: (results?: ParseQueryResults) => void
  90. }) => {
  91. if (!project) return console.error('Project is required')
  92. if (option === 'authenticated' && !user) {
  93. return toast('Select which user to test as before running the query')
  94. }
  95. try {
  96. setIsLoading(true)
  97. setSandboxError(undefined)
  98. const { appendAutoLimit } = checkIfAppendLimitRequired(value, limit)
  99. const formattedSql = suffixWithLimit(value, limit)
  100. const data = await parseQuery({ sql: formattedSql })
  101. if (data.operation !== 'SELECT') {
  102. return toast('Only SELECT statements are supported with the RLS Tester at the moment')
  103. }
  104. const formattedTables = data.tables.map((x) => {
  105. const [schema, table] = x.includes('.') ? x.split('.') : ['public', x]
  106. return { schema, table }
  107. })
  108. const response = await getTableRLSStatus({
  109. projectRef: project?.ref,
  110. connectionString: project?.connectionString,
  111. tables: formattedTables,
  112. })
  113. const tables = response
  114. .map(({ table, schema, rls_enabled }) => {
  115. const tablePolicies = filterTablePolicies({
  116. policies,
  117. schema,
  118. table,
  119. role: role?.role,
  120. operation: data.operation,
  121. })
  122. return {
  123. table,
  124. schema,
  125. isRLSEnabled: rls_enabled,
  126. tablePolicies,
  127. }
  128. })
  129. .sort((a, b) => {
  130. const aFirst = a.isRLSEnabled && a.tablePolicies.length === 0
  131. const bFirst = b.isRLSEnabled && b.tablePolicies.length === 0
  132. return Number(bFirst) - Number(aFirst)
  133. })
  134. const autoLimit = appendAutoLimit ? limit : undefined
  135. const sql = wrapWithRoleImpersonation(formattedSql, impersonatedRoleState)
  136. const { result } = sandbox
  137. ? await sandbox.run({ sql }).catch((e) => {
  138. setSandboxError(e instanceof Error ? e : new Error(String(e)))
  139. throw e
  140. })
  141. : await executeSql({
  142. sql,
  143. autoLimit,
  144. projectRef: project.ref,
  145. connectionString: project.connectionString,
  146. isRoleImpersonationEnabled: isRoleImpersonationEnabled(impersonatedRoleState.role),
  147. isStatementTimeoutDisabled: true,
  148. handleError: (e) => {
  149. throw e
  150. },
  151. queryKey: ['rls-tester'],
  152. })
  153. onExecuteSQL({ result, isAutoLimit: !!autoLimit })
  154. onParseQuery({
  155. tables,
  156. operation: data.operation,
  157. role: role?.role,
  158. user,
  159. })
  160. } catch (error) {
  161. onExecuteSQL({ result: null, isAutoLimit: false })
  162. onParseQuery(undefined)
  163. } finally {
  164. setIsLoading(false)
  165. }
  166. }
  167. return {
  168. limit,
  169. testQuery,
  170. inferSQLFromLib,
  171. isLoading,
  172. isInferring,
  173. executeSqlError,
  174. parseQueryError,
  175. parseClientCodeError,
  176. getTableRLSStatusError,
  177. }
  178. }