Files
supabase/apps/studio/data/privileges/table-api-access-mutation.ts
T
Saxon FletcherandCharis Lam 0dab4d93fe Enable or disable Data API access per table (#41218)
* feat(studio): add mutation to update table access privileges

Adds a mutation to update table grants for `anon` and `authenticated`
roles.

* feat(studio): add data api toggles

Add toggles to the Table Editor, allowing fine-grained grants to the
anon and authenticated roles.

* fix(studio): don't show rls policies warning if table not exposed

RLS policies warning was showing in Table Editor side panel even if
table was not exposed due to no grants to anon/authenticated.

* fixup! feat(studio): add data api toggles

* fixup! feat(studio): add data api toggles

* fix(studio): revalidate rls lints when table grants are toggled

---------

Co-authored-by: Charis Lam <26616127+charislam@users.noreply.github.com>
2026-01-07 12:12:15 -05:00

115 lines
3.4 KiB
TypeScript

import pgMeta from '@supabase/pg-meta'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import {
API_ACCESS_ROLES,
API_PRIVILEGE_TYPES,
type ApiPrivilegesByRole,
} from '@/lib/data-api-types'
import type { DeepReadonly } from '@/lib/type-helpers'
import { executeSql } from 'data/sql/execute-sql-query'
import type { UseCustomMutationOptions } from 'types'
import type { ConnectionVars } from '../common.types'
import { lintKeys } from '../lint/keys'
import { invalidateTablePrivilegesQuery } from './table-privileges-query'
export type TableApiAccessPrivilegesVariables = ConnectionVars & {
relationId: number
privileges: DeepReadonly<ApiPrivilegesByRole>
}
export async function updateTableApiAccessPrivileges({
projectRef,
connectionString,
relationId,
privileges,
}: TableApiAccessPrivilegesVariables) {
const sqlStatements: string[] = []
for (const role of API_ACCESS_ROLES) {
const rolePrivileges = privileges[role]
// Determine which privileges to grant and revoke for this role
const privilegesToGrant = rolePrivileges
const privilegesToRevoke = API_PRIVILEGE_TYPES.filter((p) => !rolePrivileges.includes(p))
// Revoke privileges that should be removed
if (privilegesToRevoke.length > 0) {
const revokeGrants = privilegesToRevoke.map((privilegeType) => ({
grantee: role,
privilegeType,
relationId,
}))
const revokeSql = pgMeta.tablePrivileges.revoke(revokeGrants).sql.trim()
if (revokeSql) sqlStatements.push(revokeSql)
}
// Grant privileges that should be added
if (privilegesToGrant.length > 0) {
const grantGrants = privilegesToGrant.map((privilegeType) => ({
grantee: role,
privilegeType,
relationId,
}))
const grantSql = pgMeta.tablePrivileges.grant(grantGrants).sql.trim()
if (grantSql) sqlStatements.push(grantSql)
}
}
if (sqlStatements.length === 0) {
return null
}
const { result } = await executeSql<[]>({
projectRef,
connectionString,
sql: sqlStatements.join('\n'),
queryKey: ['table-api-access', 'update-privileges'],
})
return result
}
type UpdateTableApiAccessPrivilegesData = Awaited<ReturnType<typeof updateTableApiAccessPrivileges>>
export const useTableApiAccessPrivilegesMutation = ({
onSuccess,
onError,
...options
}: Omit<
UseCustomMutationOptions<
UpdateTableApiAccessPrivilegesData,
Error,
TableApiAccessPrivilegesVariables
>,
'mutationFn'
> = {}) => {
const queryClient = useQueryClient()
return useMutation<UpdateTableApiAccessPrivilegesData, Error, TableApiAccessPrivilegesVariables>({
mutationFn: (vars) => updateTableApiAccessPrivileges(vars),
async onSuccess(data, variables, context) {
const { projectRef } = variables
await Promise.all([
invalidateTablePrivilegesQuery(queryClient, projectRef),
// This affects the result of the RLS disabled lint, so we need to
// invalidate it
queryClient.invalidateQueries({
queryKey: lintKeys.lint(projectRef),
}),
])
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
if (onError === undefined) {
toast.error(`Failed to update API access privileges: ${data.message}`)
} else {
onError(data, variables, context)
}
},
...options,
})
}