mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
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:
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;`,
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user