import { zodResolver } from '@hookform/resolvers/zod' import { Monaco } from '@monaco-editor/react' import { acceptUntrustedSql, ident, joinSqlFragments, safeSql, untrustedSql, type DisplayableSqlFragment, type SafeSqlFragment, } from '@supabase/pg-meta/src/pg-format' import { PermissionAction } from '@supabase/shared-types/out/constants' import { useQueryClient } from '@tanstack/react-query' import { useParams } from 'common' import { isEqual } from 'lodash' import { memo, useCallback, useEffect, useRef, useState } from 'react' import { useForm } from 'react-hook-form' import { toast } from 'sonner' import { Button, Checkbox, cn, Form, Label, ScrollArea, Sheet, SheetContent, SheetFooter, Tabs_Shadcn_, TabsContent_Shadcn_, TabsList_Shadcn_, TabsTrigger_Shadcn_, } from 'ui' import * as z from 'zod' import { LockedCreateQuerySection, LockedRenameQuerySection } from './LockedQuerySection' import { PolicyDetailsV2 } from './PolicyDetailsV2' import { checkIfPolicyHasChanged, generateCreatePolicyQuery } from './PolicyEditorPanel.utils' import { PolicyEditorPanelHeader } from './PolicyEditorPanelHeader' import { PolicyTemplates } from './PolicyTemplates' import { QueryError } from './QueryError' import { RLSCodeEditor } from './RLSCodeEditor' import type { Policy } from '@/components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRow.utils' import { IStandaloneCodeEditor } from '@/components/interfaces/SQLEditor/SQLEditor.types' import { DiscardChangesConfirmationDialog } from '@/components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog' import { ButtonTooltip } from '@/components/ui/ButtonTooltip' import { useDatabasePolicyUpdateMutation } from '@/data/database-policies/database-policy-update-mutation' import { databasePoliciesKeys } from '@/data/database-policies/keys' import { QueryResponseError, useExecuteSqlMutation } from '@/data/sql/execute-sql-mutation' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { useConfirmOnClose } from '@/hooks/ui/useConfirmOnClose' import { useStaticEffectEvent } from '@/hooks/useStaticEffectEvent' interface PolicyEditorPanelProps { visible: boolean schema: string searchString?: string selectedTable?: string selectedPolicy?: Policy onSelectCancel: () => void authContext: 'database' | 'realtime' } const FORM_ID = 'rls-editor' const FormSchema = z.object({ name: z.string().min(1, 'Please provide a name'), table: z.string(), behavior: z.string(), command: z.string(), roles: z.string(), }) const defaultValues = { name: '', table: '', behavior: 'permissive', command: 'select', roles: '', } /** * Using memo for this component because everything rerenders on window focus because of outside fetches */ export const PolicyEditorPanel = memo(function ({ visible, schema, searchString, selectedTable, selectedPolicy, onSelectCancel, authContext, }: PolicyEditorPanelProps) { const { ref } = useParams() const queryClient = useQueryClient() const { data: selectedProject } = useSelectedProjectQuery() const { can: canUpdatePolicies } = useAsyncCheckPermissions( PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables' ) // [Joshen] Hyrid form fields, just spit balling to get a decent POC out const [using, setUsing] = useState(undefined) const [check, setCheck] = useState(undefined) const [rolesFragment, setRolesFragment] = useState(safeSql`public`) const [fieldError, setFieldError] = useState() const [showCheckBlock, setShowCheckBlock] = useState(true) const monacoOneRef = useRef(null) const editorOneRef = useRef(null) const [expOneLineCount, setExpOneLineCount] = useState(1) const [expOneContentHeight, setExpOneContentHeight] = useState(0) const monacoTwoRef = useRef(null) const editorTwoRef = useRef(null) const [expTwoLineCount, setExpTwoLineCount] = useState(1) const [expTwoContentHeight, setExpTwoContentHeight] = useState(0) const [error, setError] = useState() const [errorPanelOpen, setErrorPanelOpen] = useState(true) const [showDetails, setShowDetails] = useState(false) const [selectedDiff, setSelectedDiff] = useState() const [showTools, setShowTools] = useState(false) const form = useForm>({ mode: 'onBlur', reValidateMode: 'onBlur', resolver: zodResolver(FormSchema as any), defaultValues, }) const { name, table, behavior, command, roles } = form.watch() const supportWithCheck = ['update', 'all'].includes(command) const isRenamingPolicy = selectedPolicy !== undefined && name !== selectedPolicy.name const { mutate: executeMutation, isPending: isExecuting } = useExecuteSqlMutation({ onSuccess: async () => { // refresh all policies await queryClient.invalidateQueries({ queryKey: databasePoliciesKeys.list(ref) }) toast.success('Successfully created new policy') onSelectCancel() }, onError: (error) => setError(error), }) const { mutate: updatePolicy, isPending: isUpdating } = useDatabasePolicyUpdateMutation({ onSuccess: () => { toast.success('Successfully updated policy') onSelectCancel() }, }) const hasUnsavedChanges = useCallback(() => { const editorOneValue = editorOneRef.current?.getValue().trim() ?? null const editorOneFormattedValue = !editorOneValue ? null : editorOneValue const editorTwoValue = editorTwoRef.current?.getValue().trim() ?? null const editorTwoFormattedValue = !editorTwoValue ? null : editorTwoValue const policyCreateUnsaved = selectedPolicy === undefined && (name.length > 0 || roles.length > 0 || !!editorOneFormattedValue || !!editorTwoFormattedValue) const policyUpdateUnsaved = selectedPolicy !== undefined ? checkIfPolicyHasChanged(selectedPolicy, { name, roles: roles.length === 0 ? ['public'] : roles.split(', '), definition: editorOneFormattedValue, check: command === 'INSERT' ? editorOneFormattedValue : editorTwoFormattedValue, }) : false return policyCreateUnsaved || policyUpdateUnsaved }, [command, name, roles, selectedPolicy]) const { confirmOnClose, handleOpenChange, modalProps } = useConfirmOnClose({ checkIsDirty: hasUnsavedChanges, onClose: onSelectCancel, }) const onSubmit = (data: z.infer) => { const { name, table, behavior, command, roles } = data // For INSERT: editor one holds the check expression (not using) // For others: editor one = using, editor two = optional check const usingExpr = command !== 'insert' ? using : undefined const checkExpr = command === 'insert' ? using : check if (command === 'insert' && !checkExpr?.trim()) { return setFieldError('Please provide a SQL expression for the WITH CHECK statement') } else if (command !== 'insert' && !usingExpr?.trim()) { return setFieldError('Please provide a SQL expression for the USING statement') } else { setFieldError(undefined) } if (selectedPolicy === undefined) { const sql = generateCreatePolicyQuery({ name, schema, table, behavior, command, roles: rolesFragment, using: usingExpr ? acceptUntrustedSql(usingExpr) : undefined, check: checkExpr ? acceptUntrustedSql(checkExpr) : undefined, }) setError(undefined) executeMutation({ sql, projectRef: selectedProject?.ref, connectionString: selectedProject?.connectionString, handleError: (error) => { throw error }, }) } else if (selectedProject !== undefined) { const payload: { name?: string definition?: SafeSqlFragment check?: SafeSqlFragment roles?: Array } = {} const updatedRoles = roles.length === 0 ? ['public'] : roles.split(', ') // Trim for string comparison against the stored policy values. The Save click is the // explicit user gesture that promotes editor content to executable SQL. const usingVal = using?.trim() const checkVal = check?.trim() if (name !== selectedPolicy.name) payload.name = name if (!isEqual(selectedPolicy.roles, updatedRoles)) payload.roles = updatedRoles if (selectedPolicy.definition !== null && selectedPolicy.definition !== usingVal) payload.definition = usingVal === undefined ? undefined : acceptUntrustedSql(untrustedSql(usingVal)) if (selectedPolicy.command === 'INSERT') { // [Joshen] Cause editor one will be the check statement in this scenario if (selectedPolicy.check !== usingVal) payload.check = usingVal === undefined ? undefined : acceptUntrustedSql(untrustedSql(usingVal)) } else { if (selectedPolicy.check !== checkVal) payload.check = checkVal === undefined ? undefined : acceptUntrustedSql(untrustedSql(checkVal)) } if (Object.keys(payload).length === 0) return onSelectCancel() updatePolicy({ projectRef: selectedProject.ref, connectionString: selectedProject?.connectionString, originalPolicy: selectedPolicy, payload, }) } } const resetState = useStaticEffectEvent(() => { if (!visible) { editorOneRef.current?.setValue('') editorTwoRef.current?.setValue('') setShowTools(false) setError(undefined) setShowDetails(false) setSelectedDiff(undefined) setUsing(undefined) setCheck(undefined) setRolesFragment(safeSql`public`) setShowCheckBlock(false) setFieldError(undefined) form.reset(defaultValues) } else { if (canUpdatePolicies) setShowTools(true) if (selectedPolicy !== undefined) { const { name, action, table, command, roles } = selectedPolicy form.reset({ name, table, behavior: action.toLowerCase(), command: command.toLowerCase(), roles: roles.length === 1 && roles[0] === 'public' ? '' : roles.join(', '), }) if (selectedPolicy.definition) setUsing(safeSql` ${selectedPolicy.definition}`) if (selectedPolicy.check && selectedPolicy.command === 'INSERT') setUsing(safeSql` ${selectedPolicy.check}`) if (selectedPolicy.check && selectedPolicy.command !== 'INSERT') { setCheck(safeSql` ${selectedPolicy.check}`) setShowCheckBlock(true) } setRolesFragment( roles.length === 1 && roles[0] === 'public' ? safeSql`public` : joinSqlFragments( roles.map((r) => ident(r)), ', ' ) ) } else if (selectedTable !== undefined) { form.reset({ ...defaultValues, table: selectedTable }) } } }) // when the panel is closed, reset all values useEffect(resetState, [visible, resetState]) // whenever the deps (current policy details, new error or error panel opens) change, recalculate // the height of the editor useEffect(() => { editorOneRef.current?.layout({ width: 0, height: 0 }) window.requestAnimationFrame(() => { editorOneRef.current?.layout() }) }, [showDetails, error, errorPanelOpen]) return ( <>
{ setFieldError(undefined) if (!['update', 'all'].includes(command)) { setShowCheckBlock(false) } else { setShowCheckBlock(true) } }} onRolesChange={(frag) => setRolesFragment(frag)} authContext={authContext} />
setUsing(untrustedSql(value ?? ''))} onChange={() => { setExpOneContentHeight(editorOneRef.current?.getContentHeight() ?? 0) setExpOneLineCount(editorOneRef.current?.getModel()?.getLineCount() ?? 1) }} onMount={() => { setTimeout(() => { setExpOneContentHeight(editorOneRef.current?.getContentHeight() ?? 0) setExpOneLineCount( editorOneRef.current?.getModel()?.getLineCount() ?? 1 ) }, 200) }} />

{7 + expOneLineCount}

{showCheckBlock ? ( <> {supportWithCheck && showCheckBlock && ( ) )} with check{' '} ( ) : ( <> ); )}

{showCheckBlock && ( <>
setCheck(untrustedSql(value ?? ''))} onChange={() => { setExpTwoContentHeight(editorTwoRef.current?.getContentHeight() ?? 0) setExpTwoLineCount( editorTwoRef.current?.getModel()?.getLineCount() ?? 1 ) }} onMount={() => { setTimeout(() => { setExpTwoContentHeight( editorTwoRef.current?.getContentHeight() ?? 0 ) setExpTwoLineCount( editorTwoRef.current?.getModel()?.getLineCount() ?? 1 ) }, 200) }} />

{8 + expOneLineCount + expTwoLineCount}

);

)} {isRenamingPolicy && ( )} {fieldError !== undefined && (

{fieldError}

)} {supportWithCheck && (
{ setFieldError(undefined) setShowCheckBlock(!showCheckBlock) }} />
)}
{error !== undefined && ( )} Save policy
{showTools && (
Templates { form.setValue('name', value.name) form.setValue('behavior', 'permissive') form.setValue('command', value.command.toLowerCase()) form.setValue('roles', value.roles.join(', ') ?? '') setUsing(safeSql` ${value.definition}`) if (value.check) { if (value.command === 'INSERT') { setUsing(safeSql` ${value.check}`) } else { setCheck(safeSql` ${value.check}`) } } setRolesFragment( value.roles.length === 0 || (value.roles.length === 1 && value.roles[0] === 'public') ? safeSql`public` : joinSqlFragments( value.roles.map((r: string) => ident(r)), ', ' ) ) setExpOneLineCount(1) setExpTwoLineCount(1) setFieldError(undefined) if (!['update', 'all'].includes(value.command.toLowerCase())) { setShowCheckBlock(false) } else if (value.check.length > 0) { setShowCheckBlock(true) } else { setShowCheckBlock(false) } }} />
)}
) }) PolicyEditorPanel.displayName = 'PolicyEditorPanel'