From 123c768de16957abc43c17312bb333cf7b09326b Mon Sep 17 00:00:00 2001 From: Monica Khoury <99693443+monicakh@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:02:00 +0300 Subject: [PATCH] Warn when authenticator role overrides exposed schemas (FE-4472) (#50982) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Adds a warning in Project Settings > API when the `authenticator` role's `pgrst.db_schemas` setting overrides the Dashboard's "Exposed schemas" configuration, plus an inline "Reset override" button to fix it in one click. ## Why `ALTER ROLE authenticator SET pgrst.db_schemas = ...` silently overrides what PostgREST actually exposes, regardless of what's selected in the Dashboard. Users hit a confusing PGRST106 error with no indication that a role-level override is the cause. ## How - New query (`authenticatorRoleConfigQueryOptions`) reads `pg_roles.rolconfig` for the `authenticator` role and parses out any `pgrst.db_schemas` value. Configured to always refetch on mount and window focus, since the fix is often applied outside the Dashboard (SQL editor, another client) with no cache-invalidation event for the app to react to. - `PostgrestConfig.tsx` compares that value against the currently selected schemas and shows an `Admonition` warning naming the actual overriding schemas, with a link to the PGRST106 troubleshooting guide, when they differ. - The warning includes a "Reset override" button that runs `alter role authenticator reset pgrst.db_schemas` after a confirmation step (showing the exact SQL that will run, with a copy button), then refetches so the warning clears immediately without a page reload. ## Testing 1. In the SQL Editor of a test project, run: ```sql alter role authenticator set pgrst.db_schemas = 'public'; ``` 2. Go to Project Settings > API, and select a schema other than (or in addition to) `public` in "Exposed schemas" (e.g. add `api`). 3. The new warning should appear, naming `public` as the schema actually in effect, with a link to the PGRST106 troubleshooting guide. 4. Click "Reset override" in the warning, confirm in the modal, and check that the warning clears immediately without a page reload. 5. Alternatively, clear the override manually from the SQL editor: ```sql alter role authenticator reset pgrst.db_schemas; ``` then navigate away from the API settings page and back (or refocus the browser tab) — the warning should clear without a hard refresh. Fixes [FE-4472](https://linear.app/supabase/issue/FE-4472/warn-when-authenticator-role-overrides-exposed-schemas) ## Summary by CodeRabbit * **New Features** * The API settings page now warns when the authenticator role’s exposed schemas differ from the saved Dashboard configuration. * You can reset the override to restore the saved schema configuration. The reset requires permission and provides success or error feedback. --- .../Settings/API/PostgrestConfig.tsx | 112 ++++++++++++++++++ .../authenticator-role-config-query.ts | 57 +++++++++ ...henticator-role-config-query.utils.test.ts | 33 ++++++ .../authenticator-role-config-query.utils.ts | 26 ++++ apps/studio/data/database/keys.ts | 2 + 5 files changed, 230 insertions(+) create mode 100644 apps/studio/data/database/authenticator-role-config-query.ts create mode 100644 apps/studio/data/database/authenticator-role-config-query.utils.test.ts create mode 100644 apps/studio/data/database/authenticator-role-config-query.utils.ts diff --git a/apps/studio/components/interfaces/Settings/API/PostgrestConfig.tsx b/apps/studio/components/interfaces/Settings/API/PostgrestConfig.tsx index e52298e6ff8..770317b3044 100644 --- a/apps/studio/components/interfaces/Settings/API/PostgrestConfig.tsx +++ b/apps/studio/components/interfaces/Settings/API/PostgrestConfig.tsx @@ -1,4 +1,5 @@ import { zodResolver } from '@hookform/resolvers/zod' +import { safeSql } from '@supabase/pg-meta' import { PermissionAction } from '@supabase/shared-types/out/constants' import { useQuery, useQueryClient } from '@tanstack/react-query' import { useParams } from 'common' @@ -24,6 +25,7 @@ import { useWatch, } from 'ui' import { Admonition } from 'ui-patterns/Admonition' +import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { MultiSelector, @@ -40,14 +42,17 @@ import { ExposedSchemaSelector, internalSchemasCannotExpose } from './ExposedSch import { HardenAPIModal } from './HardenAPIModal' import { ExposedFunctionSelector } from '@/components/interfaces/Settings/API/ExposedFunctionSelector' import { ExposedTableSelector } from '@/components/interfaces/Settings/API/ExposedTableSelector' +import CopyButton from '@/components/ui/CopyButton' import { FormActions } from '@/components/ui/Forms/FormActions' import { useProjectPostgrestConfigQuery } from '@/data/config/project-postgrest-config-query' import { useProjectPostgrestConfigUpdateMutation } from '@/data/config/project-postgrest-config-update-mutation' +import { authenticatorRoleConfigQueryOptions } from '@/data/database/authenticator-role-config-query' import { useSchemasQuery } from '@/data/database/schemas-query' import { defaultPrivilegesQueryOptions } from '@/data/privileges/default-privileges-query' import { privilegeKeys } from '@/data/privileges/keys' import { useUpdateDefaultPrivilegesMutation } from '@/data/privileges/update-default-privileges-mutation' import { useUpdateExposedEntitiesMutation } from '@/data/privileges/update-exposed-entities-mutation' +import { useExecuteSqlMutation } from '@/data/sql/execute-sql-mutation' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { useLatest } from '@/hooks/misc/useLatest' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' @@ -55,6 +60,8 @@ import { IS_PLATFORM } from '@/lib/constants' import { noop } from '@/lib/void' import type { ResponseError } from '@/types' +const resetAuthenticatorDbSchemasSql = safeSql`alter role authenticator reset pgrst.db_schemas` + const formSchema = z.object({ // Fields for updatePostgrestConfig dbSchema: z.array(z.string()), @@ -89,6 +96,7 @@ export const PostgrestConfig = () => { const queryClient = useQueryClient() const [showModal, setShowModal] = useState(false) + const [showResetOverrideConfirm, setShowResetOverrideConfirm] = useState(false) const { data: config, @@ -116,6 +124,14 @@ export const PostgrestConfig = () => { }) ) + const { data: authenticatorDbSchemasOverride, refetch: refetchAuthenticatorRoleConfig } = + useQuery( + authenticatorRoleConfigQueryOptions({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + ) + const configDbSchemas = useMemo( () => (config?.db_schema ? config.db_schema.split(',').map((x) => x.trim()) : []), [config?.db_schema] @@ -131,6 +147,18 @@ export const PostgrestConfig = () => { onError: noop, }) + const { mutate: resetAuthenticatorOverride, isPending: isResettingAuthenticatorOverride } = + useExecuteSqlMutation({ + onSuccess: async () => { + toast.success('Reset the authenticator role override') + setShowResetOverrideConfirm(false) + await refetchAuthenticatorRoleConfig() + }, + onError: (error) => { + toast.error(`Failed to reset the authenticator role override: ${error.message}`) + }, + }) + const [isUpdating, setIsUpdating] = useState(false) const formId = 'project-postgres-config' @@ -285,6 +313,16 @@ export const PostgrestConfig = () => { [watchedDbSchema] ) + const isAuthenticatorRoleOverridingSchemas = useMemo(() => { + if (!authenticatorDbSchemasOverride) return false + // Compared against the persisted config, not the live form selection, so the warning stays + // visible until the save actually succeeds rather than disappearing the moment the selector + // is edited to match. + const saved = new Set(configDbSchemas) + const overridden = new Set(authenticatorDbSchemasOverride) + return saved.size !== overridden.size || [...saved].some((schema) => !overridden.has(schema)) + }, [authenticatorDbSchemasOverride, configDbSchemas]) + return ( @@ -341,6 +379,45 @@ export const PostgrestConfig = () => { ) : null} + {isAuthenticatorRoleOverridingSchemas && ( + + The authenticator role has pgrst.db_schemas{' '} + set to{' '} + {authenticatorDbSchemasOverride?.map((schema, i) => ( + + {i > 0 && ', '} + {schema} + + ))} + , which overrides this setting. See the{' '} + + PGRST106 troubleshooting guide + {' '} + to check or reset it. + + } + actions={ + + } + /> + )} + { {IS_PLATFORM && setShowModal(false)} />} + + setShowResetOverrideConfirm(false)} + onConfirm={() => { + if (!projectRef) return + resetAuthenticatorOverride({ + projectRef, + connectionString: project?.connectionString, + sql: resetAuthenticatorDbSchemasSql, + }) + }} + > +

+ This clears the pgrst.db_schemas setting on the authenticator{' '} + role, so the Data API falls back to the schemas selected in the Dashboard above. +

+

The following statement will be run:

+
+
+            {resetAuthenticatorDbSchemasSql}
+          
+ +
+
) } diff --git a/apps/studio/data/database/authenticator-role-config-query.ts b/apps/studio/data/database/authenticator-role-config-query.ts new file mode 100644 index 00000000000..b60584bbb67 --- /dev/null +++ b/apps/studio/data/database/authenticator-role-config-query.ts @@ -0,0 +1,57 @@ +import { safeSql } from '@supabase/pg-meta' +import { queryOptions } from '@tanstack/react-query' + +import { getAuthenticatorDbSchemasOverride } from './authenticator-role-config-query.utils' +import { databaseKeys } from './keys' +import { executeSql } from '@/data/sql/execute-sql-mutation' +import type { ResponseError } from '@/types' + +export type AuthenticatorRoleConfigVariables = { + projectRef?: string + connectionString?: string | null +} + +const getAuthenticatorRoleConfigSql = safeSql` + select rolconfig from pg_roles where rolname = 'authenticator' +` + +export async function getAuthenticatorRoleConfig( + { projectRef, connectionString }: AuthenticatorRoleConfigVariables, + signal?: AbortSignal +) { + if (!projectRef) throw new Error('projectRef is required') + + const { result } = await executeSql( + { + projectRef, + connectionString, + sql: getAuthenticatorRoleConfigSql, + queryKey: ['authenticator-role-config'], + }, + signal + ) + + const rolconfig = (result[0] as { rolconfig: string[] | null } | undefined)?.rolconfig ?? null + + return getAuthenticatorDbSchemasOverride(rolconfig) +} + +export type AuthenticatorRoleConfigData = Awaited> +export type AuthenticatorRoleConfigError = ResponseError + +export const authenticatorRoleConfigQueryOptions = ({ + projectRef, + connectionString, +}: AuthenticatorRoleConfigVariables) => + queryOptions({ + // eslint-disable-next-line @tanstack/query/exhaustive-deps -- connection string doesn't change the result of the query + queryKey: databaseKeys.authenticatorRoleConfig(projectRef), + queryFn: ({ signal }) => getAuthenticatorRoleConfig({ projectRef, connectionString }, signal), + // The fix for this override is usually applied outside the Dashboard (SQL editor, another + // client), so there's no cache-invalidation event to react to. Always refetch on mount and + // window focus so navigating back to this page (or back to this browser tab) picks up a fix + // immediately, instead of silently serving a stale cached result. + refetchOnMount: 'always', + refetchOnWindowFocus: 'always', + enabled: typeof projectRef !== 'undefined', + }) diff --git a/apps/studio/data/database/authenticator-role-config-query.utils.test.ts b/apps/studio/data/database/authenticator-role-config-query.utils.test.ts new file mode 100644 index 00000000000..a6488399cc3 --- /dev/null +++ b/apps/studio/data/database/authenticator-role-config-query.utils.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from 'vitest' + +import { getAuthenticatorDbSchemasOverride } from './authenticator-role-config-query.utils' + +describe('getAuthenticatorDbSchemasOverride', () => { + it('returns null when the role has no config set', () => { + expect(getAuthenticatorDbSchemasOverride(null)).toBeNull() + }) + + it('returns null when rolconfig has no pgrst.db_schemas entry', () => { + expect(getAuthenticatorDbSchemasOverride(['search_path=public'])).toBeNull() + }) + + it('parses a single overridden schema', () => { + expect(getAuthenticatorDbSchemasOverride(['pgrst.db_schemas=public'])).toEqual(['public']) + }) + + it('parses multiple overridden schemas', () => { + expect( + getAuthenticatorDbSchemasOverride([ + 'search_path=public', + 'pgrst.db_schemas=public,custom_schema', + ]) + ).toEqual(['public', 'custom_schema']) + }) + + it('trims whitespace around schema names', () => { + expect(getAuthenticatorDbSchemasOverride(['pgrst.db_schemas=public, custom_schema'])).toEqual([ + 'public', + 'custom_schema', + ]) + }) +}) diff --git a/apps/studio/data/database/authenticator-role-config-query.utils.ts b/apps/studio/data/database/authenticator-role-config-query.utils.ts new file mode 100644 index 00000000000..d432003506d --- /dev/null +++ b/apps/studio/data/database/authenticator-role-config-query.utils.ts @@ -0,0 +1,26 @@ +/** + * Parses a Postgres `rolconfig` array literal (e.g. `{search_path=public,pgrst.db_schemas=public,custom}`) + * and returns the schemas set via `pgrst.db_schemas` on the role, if present. + * + * `ALTER ROLE authenticator SET pgrst.db_schemas = '...'` overrides the Dashboard's "Exposed + * schemas" setting at the PostgREST level, so this is used to detect that override. + */ +export function getAuthenticatorDbSchemasOverride(rolconfig: string[] | null): string[] | null { + if (!rolconfig) return null + + for (const entry of rolconfig) { + const separatorIndex = entry.indexOf('=') + if (separatorIndex === -1) continue + + const key = entry.slice(0, separatorIndex).trim() + if (key !== 'pgrst.db_schemas') continue + + const value = entry.slice(separatorIndex + 1).trim() + return value + .split(',') + .map((schema) => schema.trim()) + .filter(Boolean) + } + + return null +} diff --git a/apps/studio/data/database/keys.ts b/apps/studio/data/database/keys.ts index 82b825ce91b..7d1e515f2f4 100644 --- a/apps/studio/data/database/keys.ts +++ b/apps/studio/data/database/keys.ts @@ -72,6 +72,8 @@ export const databaseKeys = { ['projects', projectRef, 'supamonitor-enabled'] as const, databaseActivity: (projectRef: string | undefined) => ['projects', projectRef, 'database-activity'] as const, + authenticatorRoleConfig: (projectRef: string | undefined) => + ['projects', projectRef, 'authenticator-role-config'] as const, } export const getLiveTupleEstimateKey = (