PolicyTableRow.utils.ts 3.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. import { ident, joinSqlFragments, safeSql, type SafeSqlFragment } from '@supabase/pg-meta'
  2. import type { PGPolicy } from '@supabase/pg-meta'
  3. import type { TableApiAccessData } from '@/data/privileges/table-api-access-query'
  4. export type Policy = Omit<PGPolicy, 'definition' | 'check'> & {
  5. definition: SafeSqlFragment | null
  6. check: SafeSqlFragment | null
  7. }
  8. /**
  9. * Single classifier for the RLS page's per-table admonition state. Shares the
  10. * "granted / custom / revoked" grant semantics used by the Data API settings
  11. * page's ExposedTableSelector so the two views agree on what counts as exposed.
  12. */
  13. export type TableDataApiStatus =
  14. | 'schema-not-exposed' // schema isn't in the PostgREST exposed list
  15. | 'no-grants' // schema exposed, no API roles have any privileges (revoked)
  16. | 'custom-grants' // schema exposed, partial / non-standard grants
  17. | 'publicly-readable' // fully granted + RLS disabled (dangerous)
  18. | 'locked-by-rls' // fully granted + RLS enabled, no policies
  19. | 'secured' // fully granted + RLS enabled, policies exist
  20. | 'unknown' // privileges query is still loading or errored — caller should stay silent
  21. export function getTableDataApiStatus({
  22. isSchemaExposed,
  23. apiAccessData,
  24. isRLSEnabled,
  25. policiesCount,
  26. }: {
  27. isSchemaExposed: boolean
  28. apiAccessData: TableApiAccessData | undefined
  29. isRLSEnabled: boolean
  30. policiesCount: number
  31. }): TableDataApiStatus {
  32. if (!isSchemaExposed) return 'schema-not-exposed'
  33. if (apiAccessData?.apiAccessType === 'exposed-schema-no-grants') return 'no-grants'
  34. if (apiAccessData?.apiAccessType === 'access') {
  35. if (apiAccessData.grantStatus === 'custom') return 'custom-grants'
  36. if (!isRLSEnabled) return 'publicly-readable'
  37. if (policiesCount === 0) return 'locked-by-rls'
  38. return 'secured'
  39. }
  40. // Schema is exposed but the privileges query hasn't resolved (still loading
  41. // or errored). We return 'unknown' rather than 'schema-not-exposed' so the
  42. // caller doesn't falsely tell the user to reconfigure API settings.
  43. return 'unknown'
  44. }
  45. /**
  46. * Returns the copy for the in-row admonition, or null when the row needs no
  47. * admonition (the "everything is fine" `secured` case and the orthogonal
  48. * `schema-not-exposed` case which is rendered separately with a link).
  49. */
  50. export function getTableAdmonitionMessage(status: TableDataApiStatus): string | null {
  51. switch (status) {
  52. case 'custom-grants':
  53. return 'This table has custom Data API permissions — access may be restricted for some roles or operations.'
  54. case 'no-grants':
  55. return 'This table cannot be accessed via the Data API. Enable access in your project’s Data API settings.'
  56. case 'publicly-readable':
  57. return 'This table can be accessed by anyone via the Data API as RLS is disabled.'
  58. case 'locked-by-rls':
  59. return 'No data will be returned via the Data API as no RLS policies exist on this table.'
  60. default:
  61. return null
  62. }
  63. }
  64. export const generatePolicyUpdateSQL = (policy: Policy): SafeSqlFragment => {
  65. const parts: Array<SafeSqlFragment> = []
  66. if (policy.definition != null) {
  67. const semicolon = policy.check == null ? safeSql`;` : safeSql``
  68. parts.push(safeSql`using (${policy.definition})${semicolon}`)
  69. }
  70. if (policy.check != null) {
  71. parts.push(safeSql`with check (${policy.check});`)
  72. }
  73. const expression = parts.length > 0 ? joinSqlFragments(parts, '\n') : safeSql``
  74. return safeSql`
  75. alter policy ${ident(policy.name)}
  76. on ${ident(policy.schema)}.${ident(policy.table)}
  77. to ${joinSqlFragments(policy.roles.map(ident), ', ')}
  78. ${expression}`
  79. }