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