mirror of
https://github.com/supabase/supabase.git
synced 2026-05-06 08:56:46 -04:00
0433eeb5f5
Mark provenance of SQL via the branded types SafeSqlFragment and UntrustedSqlFragment. Only SafeSqlFragment should be executed; UntrustedSqlFragments require some kind of implicit user approval (show on screen + user has to click something) before they are promoted to SafeSqlFragment. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Editor and RLS tester show loading states for inferred/generated SQL and include a dedicated user SQL editor for safer edits. * **Refactor** * Platform-wide SQL handling tightened: snippets and AI-generated SQL are treated as untrusted/display-only until promoted, improving safety and consistency. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
89 lines
3.5 KiB
TypeScript
89 lines
3.5 KiB
TypeScript
import { ident, joinSqlFragments, safeSql, type SafeSqlFragment } from '@supabase/pg-meta'
|
||
import { PostgresPolicy } from '@supabase/postgres-meta'
|
||
|
||
import type { TableApiAccessData } from '@/data/privileges/table-api-access-query'
|
||
|
||
export type Policy = Omit<PostgresPolicy, 'definition' | 'check'> & {
|
||
definition: SafeSqlFragment | null
|
||
check: SafeSqlFragment | null
|
||
}
|
||
|
||
/**
|
||
* Single classifier for the RLS page's per-table admonition state. Shares the
|
||
* "granted / custom / revoked" grant semantics used by the Data API settings
|
||
* page's ExposedTableSelector so the two views agree on what counts as exposed.
|
||
*/
|
||
export type TableDataApiStatus =
|
||
| 'schema-not-exposed' // schema isn't in the PostgREST exposed list
|
||
| 'no-grants' // schema exposed, no API roles have any privileges (revoked)
|
||
| 'custom-grants' // schema exposed, partial / non-standard grants
|
||
| 'publicly-readable' // fully granted + RLS disabled (dangerous)
|
||
| 'locked-by-rls' // fully granted + RLS enabled, no policies
|
||
| 'secured' // fully granted + RLS enabled, policies exist
|
||
| 'unknown' // privileges query is still loading or errored — caller should stay silent
|
||
|
||
export function getTableDataApiStatus({
|
||
isSchemaExposed,
|
||
apiAccessData,
|
||
isRLSEnabled,
|
||
policiesCount,
|
||
}: {
|
||
isSchemaExposed: boolean
|
||
apiAccessData: TableApiAccessData | undefined
|
||
isRLSEnabled: boolean
|
||
policiesCount: number
|
||
}): TableDataApiStatus {
|
||
if (!isSchemaExposed) return 'schema-not-exposed'
|
||
if (apiAccessData?.apiAccessType === 'exposed-schema-no-grants') return 'no-grants'
|
||
if (apiAccessData?.apiAccessType === 'access') {
|
||
if (apiAccessData.grantStatus === 'custom') return 'custom-grants'
|
||
if (!isRLSEnabled) return 'publicly-readable'
|
||
if (policiesCount === 0) return 'locked-by-rls'
|
||
return 'secured'
|
||
}
|
||
// Schema is exposed but the privileges query hasn't resolved (still loading
|
||
// or errored). We return 'unknown' rather than 'schema-not-exposed' so the
|
||
// caller doesn't falsely tell the user to reconfigure API settings.
|
||
return 'unknown'
|
||
}
|
||
|
||
/**
|
||
* Returns the copy for the in-row admonition, or null when the row needs no
|
||
* admonition (the "everything is fine" `secured` case and the orthogonal
|
||
* `schema-not-exposed` case which is rendered separately with a link).
|
||
*/
|
||
export function getTableAdmonitionMessage(status: TableDataApiStatus): string | null {
|
||
switch (status) {
|
||
case 'custom-grants':
|
||
return 'This table has custom Data API permissions — access may be restricted for some roles or operations.'
|
||
case 'no-grants':
|
||
return 'This table cannot be accessed via the Data API. Enable access in your project’s Data API settings.'
|
||
case 'publicly-readable':
|
||
return 'This table can be accessed by anyone via the Data API as RLS is disabled.'
|
||
case 'locked-by-rls':
|
||
return 'No data will be returned via the Data API as no RLS policies exist on this table.'
|
||
default:
|
||
return null
|
||
}
|
||
}
|
||
|
||
export const generatePolicyUpdateSQL = (policy: Policy): SafeSqlFragment => {
|
||
const parts: Array<SafeSqlFragment> = []
|
||
|
||
if (policy.definition != null) {
|
||
const semicolon = policy.check == null ? safeSql`;` : safeSql``
|
||
parts.push(safeSql`using (${policy.definition})${semicolon}`)
|
||
}
|
||
if (policy.check != null) {
|
||
parts.push(safeSql`with check (${policy.check});`)
|
||
}
|
||
|
||
const expression = parts.length > 0 ? joinSqlFragments(parts, '\n') : safeSql``
|
||
|
||
return safeSql`
|
||
alter policy ${ident(policy.name)}
|
||
on ${ident(policy.schema)}.${ident(policy.table)}
|
||
to ${joinSqlFragments(policy.roles.map(ident), ', ')}
|
||
${expression}`
|
||
}
|