import { useParams } from 'common' import { isEqual } from 'lodash' import { HelpCircle, Settings } from 'lucide-react' import Link from 'next/link' import { useEffect, useState } from 'react' import { toast } from 'sonner' import { Button, Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetSection, SheetTitle, SheetTrigger, Switch, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Tooltip, TooltipContent, TooltipTrigger, } from 'ui' import { Admonition } from 'ui-patterns/admonition' import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import { pgmqArchiveTable, pgmqQueueTable } from '../Queues.utils' import { getQueueFunctionsMapping } from './Queue.utils' import AlertError from '@/components/ui/AlertError' import { ButtonTooltip } from '@/components/ui/ButtonTooltip' import { useQueuesExposePostgrestStatusQuery } from '@/data/database-queues/database-queues-expose-postgrest-status-query' import { useDatabaseRolesQuery } from '@/data/database-roles/database-roles-query' import { TablePrivilegesGrant, useTablePrivilegesGrantMutation, } from '@/data/privileges/table-privileges-grant-mutation' import { useTablePrivilegesQuery } from '@/data/privileges/table-privileges-query' import { TablePrivilegesRevoke, useTablePrivilegesRevokeMutation, } from '@/data/privileges/table-privileges-revoke-mutation' import { useTablesQuery } from '@/data/tables/tables-query' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' const ACTIONS = ['select', 'insert', 'update', 'delete'] const ROLES = ['anon', 'authenticated', 'postgres', 'service_role'] type Privileges = { select?: boolean; insert?: boolean; update?: boolean; delete?: boolean } interface QueueSettingsProps {} export const QueueSettings = ({}: QueueSettingsProps) => { const { childId: name } = useParams() const { data: project } = useSelectedProjectQuery() const [open, setOpen] = useState(false) const [isSaving, setIsSaving] = useState(false) const [privileges, setPrivileges] = useState<{ [key: string]: Privileges }>({}) const { data: isExposed } = useQueuesExposePostgrestStatusQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) const { data, error, isPending: isLoading, isSuccess, isError, } = useDatabaseRolesQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) const roles = (data ?? []) .filter((x) => ROLES.includes(x.name)) .sort((a, b) => a.name.localeCompare(b.name)) const { data: queueTables } = useTablesQuery({ projectRef: project?.ref, connectionString: project?.connectionString, schema: 'pgmq', }) const queueRelname = name ? pgmqQueueTable(name) : undefined const archiveRelname = name ? pgmqArchiveTable(name) : undefined const queueTable = queueTables?.find((x) => x.name === queueRelname) const archiveTable = queueTables?.find((x) => x.name === archiveRelname) const { data: allTablePrivileges, isSuccess: isSuccessPrivileges } = useTablePrivilegesQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) const queuePrivileges = allTablePrivileges?.find( (x) => x.schema === 'pgmq' && x.name === queueRelname ) const { mutateAsync: grantPrivilege } = useTablePrivilegesGrantMutation() const { mutateAsync: revokePrivilege } = useTablePrivilegesRevokeMutation() const onTogglePrivilege = (role: string, action: string, value: boolean) => { const updatedPrivileges = { ...privileges, [role]: { ...privileges[role], [action]: value } } setPrivileges(updatedPrivileges) } const onSaveConfiguration = async () => { if (!project) return console.error('Project is required') if (!queueTable) return console.error('Unable to find queue table') if (!archiveTable) return console.error('Unable to find archive table') setIsSaving(true) const revoke: { role: string; action: string }[] = [] const grant: { role: string; action: string }[] = [] Object.entries(privileges).forEach(([role, p]) => { const originalRolePrivileges = queuePrivileges?.privileges.filter((x) => x.grantee === role) Object.entries(p).forEach(([action, value]) => { const originalValue = !!originalRolePrivileges?.find( (x) => x.privilege_type.toLowerCase() === action ) if (value !== originalValue) { if (value) grant.push({ role, action }) else revoke.push({ role, action }) } }) }) const rolesBeingGrantedPerms = [...new Set(grant.map((x) => x.role))] const rolesBeingRevokedPerms = [...new Set(revoke.map((x) => x.role))] const rolesNoLongerHavingPerms = rolesBeingRevokedPerms.filter((x) => { const existingPrivileges = queuePrivileges?.privileges .filter((y) => x === y.grantee) .map((y) => y.privilege_type) const privilegesGettingRevoked = revoke .filter((y) => y.role === x) .map((y) => y.action.toUpperCase()) const privilegesGettingGranted = grant.filter((y) => y.role === x) return ( privilegesGettingGranted.length === 0 && isEqual(existingPrivileges, privilegesGettingRevoked) ) }) try { await Promise.all([ ...(revoke.length > 0 ? [ revokePrivilege({ projectRef: project.ref, connectionString: project.connectionString, revokes: revoke.map((x) => ({ grantee: x.role, privilegeType: x.action.toUpperCase(), relationId: queueTable.id, })) as TablePrivilegesRevoke[], }), ] : []), // Revoke select + insert on archive table only if role no longer has ANY perms on the queue table ...(rolesNoLongerHavingPerms.length > 0 ? [ revokePrivilege({ projectRef: project.ref, connectionString: project.connectionString, revokes: [ ...rolesNoLongerHavingPerms.map((x) => ({ grantee: x, privilegeType: 'INSERT' as const, relationId: archiveTable.id, })), ...rolesNoLongerHavingPerms.map((x) => ({ grantee: x, privilegeType: 'SELECT' as const, relationId: archiveTable.id, })), ], }), ] : []), ...(grant.length > 0 ? [ grantPrivilege({ projectRef: project.ref, connectionString: project.connectionString, grants: grant.map((x) => ({ grantee: x.role, privilegeType: x.action.toUpperCase(), relationId: queueTable.id, })) as TablePrivilegesGrant[], }), // Just grant select + insert on archive table as long as we're granting any perms to the queue table for the role grantPrivilege({ projectRef: project.ref, connectionString: project.connectionString, grants: [ ...rolesBeingGrantedPerms.map((x) => ({ grantee: x, privilegeType: 'INSERT' as const, relationId: archiveTable.id, })), ...rolesBeingGrantedPerms.map((x) => ({ grantee: x, privilegeType: 'SELECT' as const, relationId: archiveTable.id, })), ], }), ] : []), ]) toast.success('Successfully updated permissions') setOpen(false) } catch (error: any) { toast.error(`Failed to update permissions: ${error.message}`) } finally { setIsSaving(false) } } useEffect(() => { if (open && isSuccessPrivileges && queuePrivileges) { const initialState = queuePrivileges.privileges.reduce((a, b) => { return { ...a, [b.grantee]: { ...(a as any)[b.grantee], [b.privilege_type.toLowerCase()]: true }, } }, {}) setPrivileges(initialState) } }, [open, isSuccessPrivileges]) return ( } title="Settings" tooltip={{ content: { side: 'bottom', text: 'Queue settings' } }} /> Manage queue permissions on {name} Configure permissions for the following roles to grant access to the relevant actions on the queue.{' '} {isExposed && ( <> These will also determine access to each function available from the{' '} pgmq_public schema. )} {!isExposed ? ( You may opt to manage your queues via any Briven client libraries or PostgREST endpoints by enabling this in the{' '} queues settings } /> ) : ( )} Role {ACTIONS.map((x) => { const relatedFunctions = getQueueFunctionsMapping(x) return ( {x} {isExposed && } {isExposed && (

Required for{' '} {relatedFunctions.length === 6 ? 'all' : `the following ${relatedFunctions.length}`}{' '} functions:

{relatedFunctions.map((y) => ( {y} ))}
)}
) })}
{isLoading && ( <> )} {isError && ( )} {isSuccess && (roles ?? []).map((role) => { return ( {role.name} {ACTIONS.map((x) => ( onTogglePrivilege(role.name, x, value)} /> ))} ) })}
) }