From d65562d1d4da5ab4ecefb788cd42fe00f1f0532e Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Thu, 15 Aug 2024 10:55:58 -0400 Subject: [PATCH] fix: apply grants on create / update / delete auth postgres hook (#28609) * fix: apply grants on create / update / delete auth postgres hook * fix: move executeSql into onSuccess callback * fix: use useMemo to get permission changes * fix: standardize padding * fix: increase size of delete modal * Minor fixes for the useMemo and the divs for the code editor. * chore: resolve comment * Reorder the code. --------- Co-authored-by: Ivan Vasilov --- .../interfaces/Auth/Hooks/CreateHookSheet.tsx | 173 ++++++++++++------ .../interfaces/Auth/Hooks/HooksListing.tsx | 49 ++++- .../interfaces/Auth/Hooks/hooks.utils.ts | 14 ++ 3 files changed, 173 insertions(+), 63 deletions(-) diff --git a/apps/studio/components/interfaces/Auth/Hooks/CreateHookSheet.tsx b/apps/studio/components/interfaces/Auth/Hooks/CreateHookSheet.tsx index 2d5027d9f45..6a1f575a630 100644 --- a/apps/studio/components/interfaces/Auth/Hooks/CreateHookSheet.tsx +++ b/apps/studio/components/interfaces/Auth/Hooks/CreateHookSheet.tsx @@ -8,9 +8,12 @@ import ReactMarkdown from 'react-markdown' import * as z from 'zod' import { useParams } from 'common' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import CodeEditor from 'components/ui/CodeEditor/CodeEditor' import SchemaSelector from 'components/ui/SchemaSelector' import { AuthConfigResponse } from 'data/auth/auth-config-query' import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation' +import { executeSql } from 'data/sql/execute-sql-query' import { useFlag } from 'hooks/ui/useFlag' import { Button, @@ -33,7 +36,7 @@ import { import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import FunctionSelector from './FunctionSelector' import { HOOKS_DEFINITIONS, HOOK_DEFINITION_TITLE, Hook } from './hooks.constants' -import { extractMethod, isValidHook } from './hooks.utils' +import { extractMethod, getRevokePermissionStatements, isValidHook } from './hooks.utils' interface CreateHookSheetProps { visible: boolean @@ -110,6 +113,7 @@ export const CreateHookSheet = ({ authConfig, }: CreateHookSheetProps) => { const { ref: projectRef } = useParams() + const { project } = useProjectContext() const { mutate: updateAuthConfig, isLoading: isUpdatingConfig } = useAuthConfigUpdateMutation() const httpsAuthHooksEnabled = useFlag('httpsAuthHooksEnabled') @@ -118,14 +122,16 @@ export const CreateHookSheet = ({ [title] ) - const hook: Hook = { - ...definition, - enabled: authConfig?.[definition.enabledKey] || false, - method: extractMethod( - authConfig?.[definition.uriKey] || '', - authConfig?.[definition.secretsKey] || '' - ), - } + const hook: Hook = useMemo(() => { + return { + ...definition, + enabled: authConfig?.[definition.enabledKey] || false, + method: extractMethod( + authConfig?.[definition.uriKey] || '', + authConfig?.[definition.secretsKey] || '' + ), + } + }, [definition, authConfig]) // if the hook has all parameters, then it is not being created. const isCreating = !isValidHook(hook) @@ -184,9 +190,38 @@ export const CreateHookSheet = ({ }) } } - }, [authConfig, title, visible]) + }, [authConfig, title, visible, definition]) + + const values = form.watch() + + const statements = useMemo(() => { + let permissionChanges: string[] = [] + if (hook.method.type === 'postgres') { + if ( + hook.method.schema !== '' && + hook.method.functionName !== '' && + hook.method.functionName !== values.postgresValues.functionName + ) { + permissionChanges = getRevokePermissionStatements( + hook.method.schema, + hook.method.functionName + ) + } + } + + if (values.postgresValues.functionName !== '') { + permissionChanges = [ + ...permissionChanges, + `-- Grant access to function to supabase_auth_admin\ngrant execute on function ${values.postgresValues.schema}.${values.postgresValues.functionName} to supabase_auth_admin;`, + `-- Grant access to schema to supabase_auth_admin\ngrant usage on schema ${values.postgresValues.schema} to supabase_auth_admin;`, + `-- Revoke function permissions from authenticated, anon and public\nrevoke execute on function ${values.postgresValues.schema}.${values.postgresValues.functionName} from authenticated, anon, public;`, + ] + } + return permissionChanges + }, [hook, values.postgresValues.schema, values.postgresValues.functionName]) const onSubmit: SubmitHandler> = async (values) => { + if (!project) return console.error('Project is required') const definition = HOOKS_DEFINITIONS.find((d) => values.hookType === d.title) if (!definition) { @@ -215,14 +250,23 @@ export const CreateHookSheet = ({ { onSuccess: () => { toast.success(`Successfully created ${values.hookType}.`) + if (statements.length > 0) { + executeSql({ + projectRef, + connectionString: project.connectionString, + sql: statements.join('\n'), + }) + } + onClose() + }, + onError: (error) => { + toast.error(`Failed to create hook: ${error.message}`) onClose() }, } ) } - const values = form.getValues() - return ( onClose()}> @@ -276,7 +320,7 @@ export const CreateHookSheet = ({ field.onChange(value)} > )} {values.selectedType === 'postgres' ? ( -
- ( - - - { - field.onChange(name) - }} - disabled={field.disabled} - /> - - - )} - /> - ( - - - - - - )} - /> -
+ <> +
+ ( + + + field.onChange(name)} + disabled={field.disabled} + /> + + + )} + /> + ( + + + + + + )} + /> +
+
+

+ The following statements will be executed on the function: +

+ +
+ ) : (
{ const { ref: projectRef } = useParams() + const { project } = useProjectContext() const { data: authConfig, error: authConfigError, isError } = useAuthConfigQuery({ projectRef }) const [selectedHook, setSelectedHook] = useState(null) @@ -103,6 +108,7 @@ export const HooksListing = () => { { if (!selectedHookForDeletion) { return } + await updateAuthConfig({ projectRef: projectRef!, config: { @@ -120,14 +127,48 @@ export const HooksListing = () => { [selectedHookForDeletion.secretsKey]: null, }, }) + + const { method } = selectedHookForDeletion + + if (method.type === 'postgres') { + const revokeStatements = getRevokePermissionStatements( + method.schema, + method.functionName + ) + await executeSql({ + projectRef, + connectionString: project!.connectionString, + sql: revokeStatements.join('\n'), + }) + } toast.success(`${selectedHookForDeletion.title} has been deleted.`) setSelectedHookForDeletion(null) setSelectedHook(null) }} > -

- {`Are you sure you want to delete the ${selectedHookForDeletion?.title}?`} -

+
+

+ {`Are you sure you want to delete the ${selectedHookForDeletion?.title}?`} +

+ {selectedHookForDeletion?.method.type === 'postgres' && ( + <> +

+ {`The following statements will be executed on the ${selectedHookForDeletion?.method.schema}.${selectedHookForDeletion?.method.functionName} function:`} +

+
+ +
+ + )} +
) diff --git a/apps/studio/components/interfaces/Auth/Hooks/hooks.utils.ts b/apps/studio/components/interfaces/Auth/Hooks/hooks.utils.ts index 7a801cca980..f77d58365ca 100644 --- a/apps/studio/components/interfaces/Auth/Hooks/hooks.utils.ts +++ b/apps/studio/components/interfaces/Auth/Hooks/hooks.utils.ts @@ -27,3 +27,17 @@ export const isValidHook = (h: Hook) => { (h.method.type === 'https' && h.method.url.startsWith('https') && h.method.secret.length > 0) ) } + +/** + * + * @param schema the schema that the function belongs to + * @param functionName the function name associated with the hook + * @returns an array of SQL statements to restore the original permissions to the function + */ +export const getRevokePermissionStatements = (schema: string, functionName: string): string[] => { + return [ + `-- Revoke access to function from supabase_auth_admin\nrevoke execute on function ${schema}.${functionName} from supabase_auth_admin;`, + `-- Revoke access to schema from supabase_auth_admin\nrevoke usage on schema ${schema} from supabase_auth_admin;`, + `-- Restore function permissions to authenticated, anon and public\ngrant execute on function ${schema}.${functionName} to authenticated, anon, public;`, + ] +}