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 <vasilov.ivan@gmail.com>
This commit is contained in:
Kang MingandIvan Vasilov authored and GitHub committed 2024-08-15 10:55:58 -04:00
1 parent 39cc19de5b
commit d65562d1d4
3 files changed
+173 -63

No files matched your search

@@ -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<z.infer<typeof FormSchema>> = 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 (
<Sheet open={visible} onOpenChange={() => onClose()}>
<SheetContent size="lg" showClose={false} className="flex flex-col gap-0">
@@ -276,7 +320,7 @@ export const CreateHookSheet = ({
<FormItemLayout label="Hook type" className="px-8">
<FormControl_Shadcn_>
<RadioGroupStacked
value={values.selectedType}
value={field.value}
onValueChange={(value) => field.onChange(value)}
>
<RadioGroupStackedItem
@@ -300,52 +344,63 @@ export const CreateHookSheet = ({
/>
)}
{values.selectedType === 'postgres' ? (
<div className="grid grid-cols-2 gap-8 px-8">
<FormField_Shadcn_
key="postgresValues.schema"
control={form.control}
name="postgresValues.schema"
render={({ field }) => (
<FormItemLayout
label="Postgres Schema"
description="Postgres schema where the function is defined."
>
<FormControl_Shadcn_>
<SchemaSelector
size="small"
showError={false}
selectedSchemaName={field.value}
onSelectSchema={(name) => {
field.onChange(name)
}}
disabled={field.disabled}
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
<FormField_Shadcn_
key="postgresValues.functionName"
control={form.control}
name="postgresValues.functionName"
render={({ field }) => (
<FormItemLayout
label="Function name"
description="Postgres function which will be called by Supabase Auth each time the hook is triggered."
>
<FormControl_Shadcn_>
<FunctionSelector
size="small"
schema={values.postgresValues.schema}
value={field.value}
onChange={field.onChange}
disabled={field.disabled}
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
</div>
<>
<div className="grid grid-cols-2 gap-8 px-8">
<FormField_Shadcn_
key="postgresValues.schema"
control={form.control}
name="postgresValues.schema"
render={({ field }) => (
<FormItemLayout
label="Postgres Schema"
description="Postgres schema where the function is defined."
>
<FormControl_Shadcn_>
<SchemaSelector
size="small"
showError={false}
selectedSchemaName={field.value}
onSelectSchema={(name) => field.onChange(name)}
disabled={field.disabled}
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
<FormField_Shadcn_
key="postgresValues.functionName"
control={form.control}
name="postgresValues.functionName"
render={({ field }) => (
<FormItemLayout
label="Function name"
description="Postgres function which will be called by Supabase Auth each time the hook is triggered."
>
<FormControl_Shadcn_>
<FunctionSelector
size="small"
schema={values.postgresValues.schema}
value={field.value}
onChange={field.onChange}
disabled={field.disabled}
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
</div>
<div className="h-72 w-full px-8 gap-3 flex flex-col">
<p className="text-sm text-foreground-light">
The following statements will be executed on the function:
</p>
<CodeEditor
id="postgres-hook-editor"
isReadOnly={true}
language="pgsql"
value={statements.join('\n\n')}
/>
</div>
</>
) : (
<div className="flex flex-col px-8 gap-4">
<FormField_Shadcn_
@@ -13,10 +13,15 @@ import { AddHookDropdown } from './AddHookDropdown'
import { CreateHookSheet } from './CreateHookSheet'
import { HookCard } from './HookCard'
import { HOOKS_DEFINITIONS, HOOK_DEFINITION_TITLE, Hook } from './hooks.constants'
import { extractMethod, isValidHook } from './hooks.utils'
import { extractMethod, getRevokePermissionStatements, isValidHook } from './hooks.utils'
import { executeSql } from 'data/sql/execute-sql-query'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import CodeEditor from 'components/ui/CodeEditor/CodeEditor'
import { cn } from 'ui'
export const HooksListing = () => {
const { ref: projectRef } = useParams()
const { project } = useProjectContext()
const { data: authConfig, error: authConfigError, isError } = useAuthConfigQuery({ projectRef })
const [selectedHook, setSelectedHook] = useState<HOOK_DEFINITION_TITLE | null>(null)
@@ -103,6 +108,7 @@ export const HooksListing = () => {
<ConfirmationModal
visible={!!selectedHookForDeletion}
size="large"
variant="destructive"
title="Confirm to delete"
confirmLabel="Delete"
@@ -112,6 +118,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)
}}
>
<p className="py-4 text-sm text-foreground-light">
{`Are you sure you want to delete the ${selectedHookForDeletion?.title}?`}
</p>
<div>
<p className="py-4 text-sm text-foreground-light">
{`Are you sure you want to delete the ${selectedHookForDeletion?.title}?`}
</p>
{selectedHookForDeletion?.method.type === 'postgres' && (
<>
<p className="py-4 text-sm text-foreground-light">
{`The following statements will be executed on the ${selectedHookForDeletion?.method.schema}.${selectedHookForDeletion?.method.functionName} function:`}
</p>
<div className={cn('h-72')}>
<CodeEditor
id="deletion-hook-editor"
isReadOnly={true}
language="pgsql"
value={getRevokePermissionStatements(
selectedHookForDeletion?.method.schema,
selectedHookForDeletion?.method.functionName
).join('\n\n')}
/>
</div>
</>
)}
</div>
</ConfirmationModal>
</div>
)
@@ -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;`,
]
}