Files
supabase/apps/studio/data/privileges/table-api-access-query.ts
T
Alaister Young 9652e8c4d3 [SEC-385] feat(studio): Add default privileges for new entities toggle (#43583)
Adds a new toggle in:
<img width="1161" height="356" alt="Screenshot 2026-03-10 at 17 17 06"
src="https://github.com/user-attachments/assets/b09ac1aa-a8f5-4fb4-8771-f113b140eac8"
/>

Other changes:
- form submissions with no table/function changes were failing because
an empty string got passed to executeSql. Added an early return when
there's nothing to execute

To test:
- Ensure the form is still in working order
- Create some tables and functions with the toggle on add off and make
sure your selected default applies
2026-03-12 09:38:03 +00:00

219 lines
5.6 KiB
TypeScript

import type { ConnectionVars } from 'data/common.types'
import { useIsSchemaExposed } from 'hooks/misc/useIsSchemaExposed'
import { isApiAccessRole, isApiPrivilegeType, type ApiPrivilegesByRole } from 'lib/data-api-types'
import type { Prettify } from 'lib/type-helpers'
import { useMemo } from 'react'
import type { UseCustomQueryOptions } from 'types'
import {
useTablePrivilegesQuery,
type TablePrivilegesData,
type TablePrivilegesError,
} from './table-privileges-query'
// The contents of this array are never used, so any will allow
// it to be used anywhere an array of any type is required.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const STABLE_EMPTY_ARRAY: any[] = []
const STABLE_EMPTY_OBJECT = {}
const getApiPrivilegesByRole = (
privileges: TablePrivilegesData[number]['privileges']
): ApiPrivilegesByRole => {
const privilegesByRole: ApiPrivilegesByRole = {
anon: [],
authenticated: [],
service_role: [],
}
privileges.forEach((privilege) => {
const { grantee, privilege_type } = privilege
if (isApiAccessRole(grantee) && isApiPrivilegeType(privilege_type)) {
privilegesByRole[grantee].push(privilege_type)
}
})
return privilegesByRole
}
const mapPrivilegesByTableName = (
privileges: TablePrivilegesData | undefined,
schemaName: string,
tableNames: Set<string>
): Record<string, ApiPrivilegesByRole> => {
if (!privileges) return {}
const result: Record<string, ApiPrivilegesByRole> = {}
privileges.forEach((entry) => {
if (entry.schema !== schemaName) return
if (!tableNames.has(entry.name)) return
result[entry.name] = getApiPrivilegesByRole(entry.privileges)
})
return result
}
export type UseTableApiAccessQueryParams = Prettify<
ConnectionVars & {
schemaName: string
tableNames: string[]
}
>
export type DataApiAccessType = 'none' | 'exposed-schema-no-grants' | 'access'
export type TableApiAccessData =
| {
apiAccessType: 'access'
privileges: ApiPrivilegesByRole
}
| {
apiAccessType: 'none' | 'exposed-schema-no-grants'
}
export type TableApiAccessMap = Prettify<Record<string, TableApiAccessData>>
export type UseTableApiAccessQueryReturn =
| {
data: TableApiAccessMap
status: 'success'
isSuccess: true
isPending: false
isError: false
}
| {
data: undefined
status: 'pending'
isSuccess: false
isPending: true
isError: false
}
| {
data: undefined
status: 'error'
isSuccess: false
isPending: false
isError: true
}
export const useTableApiAccessQuery = (
{
projectRef,
connectionString,
schemaName,
tableNames = STABLE_EMPTY_ARRAY,
}: UseTableApiAccessQueryParams,
{
enabled = true,
...options
}: { enabled?: boolean } & Omit<
UseCustomQueryOptions<TablePrivilegesData, TablePrivilegesError>,
'enabled'
> = {}
): UseTableApiAccessQueryReturn => {
const uniqueTableNames = useMemo(() => {
return new Set(
tableNames.filter((tableName) => typeof tableName === 'string' && tableName.length > 0)
)
}, [tableNames])
const hasTables = uniqueTableNames.size > 0
const schemaExposureStatus = useIsSchemaExposed({ projectRef, schemaName }, { enabled })
const isSchemaExposed = schemaExposureStatus.isSuccess && schemaExposureStatus.data === true
const enablePrivilegesQuery = enabled && hasTables
const privilegeStatus = useTablePrivilegesQuery(
{ projectRef, connectionString },
{ enabled: enablePrivilegesQuery, ...options }
)
const result: UseTableApiAccessQueryReturn = useMemo(() => {
const isPending =
!enabled ||
schemaExposureStatus.status === 'pending' ||
(enablePrivilegesQuery && privilegeStatus.isPending)
if (isPending) {
return {
data: undefined,
status: 'pending',
isSuccess: false,
isPending: true,
isError: false,
}
}
const isError =
schemaExposureStatus.status === 'error' || (enablePrivilegesQuery && privilegeStatus.isError)
if (isError) {
return {
data: undefined,
status: 'error',
isSuccess: false,
isPending: false,
isError: true,
}
}
if (!hasTables) {
return {
data: STABLE_EMPTY_OBJECT,
status: 'success',
isSuccess: true,
isPending: false,
isError: false,
}
}
const resultData: TableApiAccessMap = {}
const tablePrivilegesByName = isSchemaExposed
? mapPrivilegesByTableName(privilegeStatus.data, schemaName, uniqueTableNames)
: {}
uniqueTableNames.forEach((tableName) => {
if (!isSchemaExposed) {
resultData[tableName] = { apiAccessType: 'none' }
return
}
const tablePrivileges = tablePrivilegesByName[tableName] ?? {
anon: [],
authenticated: [],
service_role: [],
}
const hasApiPrivileges =
tablePrivileges.anon.length > 0 ||
tablePrivileges.authenticated.length > 0 ||
tablePrivileges.service_role.length > 0
resultData[tableName] = hasApiPrivileges
? {
apiAccessType: 'access',
privileges: tablePrivileges,
}
: { apiAccessType: 'exposed-schema-no-grants' }
})
return {
data: resultData,
status: 'success',
isSuccess: true,
isPending: false,
isError: false,
}
}, [
enabled,
enablePrivilegesQuery,
hasTables,
schemaExposureStatus.status,
isSchemaExposed,
privilegeStatus.isPending,
privilegeStatus.isError,
privilegeStatus.data,
schemaName,
uniqueTableNames,
])
return result
}