import { useQueryClient } from '@tanstack/react-query' import { useCallback, useState } from 'react' import { ALL_PRIVILEGE_TYPES, COLUMN_PRIVILEGE_TYPES, ColumnPrivilegeType, } from './Privileges.constants' import { grantColumnPrivileges } from '@/data/privileges/column-privileges-grant-mutation' import type { ColumnPrivilege } from '@/data/privileges/column-privileges-query' import { ColumnPrivilegesRevoke, revokeColumnPrivileges, } from '@/data/privileges/column-privileges-revoke-mutation' import { privilegeKeys } from '@/data/privileges/keys' import { grantTablePrivileges, TablePrivilegesGrant, } from '@/data/privileges/table-privileges-grant-mutation' import type { PgTablePrivileges } from '@/data/privileges/table-privileges-query' import { revokeTablePrivileges, TablePrivilegesRevoke, } from '@/data/privileges/table-privileges-revoke-mutation' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' export interface PrivilegeOperation { object: 'table' | 'column' type: 'grant' | 'revoke' id: string | number grantee: string privilege_type: string } export function getDefaultTableCheckedStates(tablePrivilege: PgTablePrivileges) { return Object.fromEntries( ALL_PRIVILEGE_TYPES.map((privilege) => [ privilege, tablePrivilege.privileges.find((p) => p.privilege_type === privilege) !== undefined, ]) ) } export function getDefaultColumnCheckedStates(columnPrivileges: ColumnPrivilege[]) { return Object.fromEntries( columnPrivileges.map((column) => [ column.column_id, Object.fromEntries( COLUMN_PRIVILEGE_TYPES.map((privilege) => [ privilege, column.privileges.find((p) => p.privilege_type === privilege) !== undefined, ]) ), ]) ) } interface UsePrivilegesStateOptions { tableId: number role: string defaultTableCheckedStates: ReturnType defaultColumnCheckedStates: ReturnType } function addOrRemoveOperation( operations: PrivilegeOperation[], operation: PrivilegeOperation, /** removes old operations and always adds the new one */ forceAdd = false ): PrivilegeOperation[] { let state = [...operations] const oppositeType = operation.type === 'grant' ? 'revoke' : 'grant' const existing = state.find((op) => { return ( op.object === operation.object && op.type === oppositeType && op.id === operation.id && op.grantee === operation.grantee && op.privilege_type === operation.privilege_type ) }) if (existing !== undefined) { state = state.filter((op) => op !== existing) if (!forceAdd) { return state } } state.push(operation) return state } export function usePrivilegesState({ defaultTableCheckedStates, defaultColumnCheckedStates, tableId, role, }: UsePrivilegesStateOptions) { const [operations, setOperations] = useState([]) const tableCheckedStates = operations.reduce((acc, op) => { if (op.object === 'table' && op.id === tableId && op.grantee === role) { return { ...acc, [op.privilege_type]: op.type === 'grant', } } return acc }, defaultTableCheckedStates) const columnCheckedStates = operations.reduce((acc, op) => { let curr = acc if (op.object === 'table' && op.grantee === role) { curr = Object.fromEntries( Object.entries(curr).map(([id, column]) => [ id, Object.fromEntries( Object.entries(column).map(([privilege, value]) => [ privilege, op.privilege_type === privilege ? op.type === 'grant' : value, ]) ), ]) ) } if (op.object === 'column' && op.grantee === role) { return { ...curr, [op.id]: { ...curr[op.id], [op.privilege_type]: op.type === 'grant', }, } } return curr }, defaultColumnCheckedStates) function toggleTablePrivilege(privilegeType: string) { const shouldGrant = !tableCheckedStates[privilegeType] setOperations((prevState) => { let state = [...prevState] if (COLUMN_PRIVILEGE_TYPES.includes(privilegeType as ColumnPrivilegeType)) { if (shouldGrant) { // remove all operations for the columns since // the table privilege will take precedence state = state.filter( (op) => !( op.object === 'column' && op.grantee === role && op.privilege_type === privilegeType ) ) } } state = addOrRemoveOperation(state, { object: 'table', type: shouldGrant ? 'grant' : 'revoke', id: tableId, grantee: role, privilege_type: privilegeType, }) return state }) } function toggleColumnPrivilege(columnId: string, privilegeType: string) { const shouldGrant = !columnCheckedStates[columnId][privilegeType] setOperations((prevState) => { let state = [...prevState] // if the user is revoking a column and the table is enabled if (!shouldGrant && tableCheckedStates[privilegeType]) { // also revoke the table privilege state = addOrRemoveOperation(state, { object: 'table', type: 'revoke', id: tableId, grantee: role, privilege_type: privilegeType, }) // grant all other enabled columns const operations = Object.entries(columnCheckedStates) .filter(([id]) => id !== columnId) .map(([id, column]) => ({ object: 'column' as const, type: column[privilegeType] ? ('grant' as const) : ('revoke' as const), id, grantee: role, privilege_type: privilegeType, })) operations.forEach((op) => { state = addOrRemoveOperation(state, op) }) } if (shouldGrant) { const areAllOtherColumnsEnabled = Object.entries(columnCheckedStates).every( ([id, column]) => id === columnId || column[privilegeType] ) if (areAllOtherColumnsEnabled) { // remove all operations for the columns since // the table privilege will take precedence state = state.filter( (op) => !( op.object === 'column' && op.grantee === role && op.privilege_type === privilegeType ) ) // grant the table privilege state = addOrRemoveOperation(state, { object: 'table', type: 'grant', id: tableId, grantee: role, privilege_type: privilegeType, }) return state } } state = addOrRemoveOperation(state, { object: 'column', type: shouldGrant ? 'grant' : 'revoke', id: columnId, grantee: role, privilege_type: privilegeType, }) return state }) } const resetOperations = useCallback(() => { setOperations([]) }, []) return { tableCheckedStates, columnCheckedStates, operations, toggleTablePrivilege, toggleColumnPrivilege, resetOperations, } } export function useApplyPrivilegeOperations(callback?: () => void) { const { data: project } = useSelectedProjectQuery() const queryClient = useQueryClient() const [isLoading, setIsLoading] = useState(false) const apply = useCallback( async (operations: PrivilegeOperation[]) => { if (!project) return console.error('No project selected') setIsLoading(true) const tableOperations = operations.filter((op) => op.object === 'table') const columnOperations = operations.filter((op) => op.object === 'column') const grantTableOperations = tableOperations .filter((op) => op.type === 'grant') .map((op) => ({ relationId: Number(op.id), grantee: op.grantee, privilegeType: op.privilege_type as TablePrivilegesGrant['privilegeType'], })) const revokeTableOperations = tableOperations .filter((op) => op.type === 'revoke') .map((op) => ({ relationId: Number(op.id), grantee: op.grantee, privilegeType: op.privilege_type as TablePrivilegesRevoke['privilegeType'], })) const grantColumnOperations = columnOperations .filter((op) => op.type === 'grant') .map((op) => ({ column_id: String(op.id), grantee: op.grantee, privilege_type: op.privilege_type as ColumnPrivilegesRevoke['privilege_type'], })) const revokeColumnOperations = columnOperations .filter((op) => op.type === 'revoke') .map((op) => ({ column_id: String(op.id), grantee: op.grantee, privilege_type: op.privilege_type as ColumnPrivilegesRevoke['privilege_type'], })) // annoyingly these can't be run all at once // as postgres can't process them in parallel if (revokeTableOperations.length > 0) { await revokeTablePrivileges({ projectRef: project.ref, connectionString: project.connectionString, revokes: revokeTableOperations, }) } if (grantTableOperations.length > 0) { await grantTablePrivileges({ projectRef: project.ref, connectionString: project.connectionString, grants: grantTableOperations, }) } if (revokeColumnOperations.length > 0) { await revokeColumnPrivileges({ projectRef: project.ref, connectionString: project.connectionString, revokes: revokeColumnOperations, }) } if (grantColumnOperations.length > 0) { await grantColumnPrivileges({ projectRef: project.ref, connectionString: project.connectionString, grants: grantColumnOperations, }) } await Promise.all([ queryClient.invalidateQueries({ queryKey: privilegeKeys.tablePrivilegesList(project.ref) }), queryClient.invalidateQueries({ queryKey: privilegeKeys.columnPrivilegesList(project.ref), }), ]) setIsLoading(false) callback?.() }, [callback, project, queryClient] ) return { apply, isLoading } }