| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360 |
- 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<typeof getDefaultTableCheckedStates>
- defaultColumnCheckedStates: ReturnType<typeof getDefaultColumnCheckedStates>
- }
- 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<PrivilegeOperation[]>([])
- 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 }
- }
|