Privileges.utils.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. import { useQueryClient } from '@tanstack/react-query'
  2. import { useCallback, useState } from 'react'
  3. import {
  4. ALL_PRIVILEGE_TYPES,
  5. COLUMN_PRIVILEGE_TYPES,
  6. ColumnPrivilegeType,
  7. } from './Privileges.constants'
  8. import { grantColumnPrivileges } from '@/data/privileges/column-privileges-grant-mutation'
  9. import type { ColumnPrivilege } from '@/data/privileges/column-privileges-query'
  10. import {
  11. ColumnPrivilegesRevoke,
  12. revokeColumnPrivileges,
  13. } from '@/data/privileges/column-privileges-revoke-mutation'
  14. import { privilegeKeys } from '@/data/privileges/keys'
  15. import {
  16. grantTablePrivileges,
  17. TablePrivilegesGrant,
  18. } from '@/data/privileges/table-privileges-grant-mutation'
  19. import type { PgTablePrivileges } from '@/data/privileges/table-privileges-query'
  20. import {
  21. revokeTablePrivileges,
  22. TablePrivilegesRevoke,
  23. } from '@/data/privileges/table-privileges-revoke-mutation'
  24. import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
  25. export interface PrivilegeOperation {
  26. object: 'table' | 'column'
  27. type: 'grant' | 'revoke'
  28. id: string | number
  29. grantee: string
  30. privilege_type: string
  31. }
  32. export function getDefaultTableCheckedStates(tablePrivilege: PgTablePrivileges) {
  33. return Object.fromEntries(
  34. ALL_PRIVILEGE_TYPES.map((privilege) => [
  35. privilege,
  36. tablePrivilege.privileges.find((p) => p.privilege_type === privilege) !== undefined,
  37. ])
  38. )
  39. }
  40. export function getDefaultColumnCheckedStates(columnPrivileges: ColumnPrivilege[]) {
  41. return Object.fromEntries(
  42. columnPrivileges.map((column) => [
  43. column.column_id,
  44. Object.fromEntries(
  45. COLUMN_PRIVILEGE_TYPES.map((privilege) => [
  46. privilege,
  47. column.privileges.find((p) => p.privilege_type === privilege) !== undefined,
  48. ])
  49. ),
  50. ])
  51. )
  52. }
  53. interface UsePrivilegesStateOptions {
  54. tableId: number
  55. role: string
  56. defaultTableCheckedStates: ReturnType<typeof getDefaultTableCheckedStates>
  57. defaultColumnCheckedStates: ReturnType<typeof getDefaultColumnCheckedStates>
  58. }
  59. function addOrRemoveOperation(
  60. operations: PrivilegeOperation[],
  61. operation: PrivilegeOperation,
  62. /** removes old operations and always adds the new one */
  63. forceAdd = false
  64. ): PrivilegeOperation[] {
  65. let state = [...operations]
  66. const oppositeType = operation.type === 'grant' ? 'revoke' : 'grant'
  67. const existing = state.find((op) => {
  68. return (
  69. op.object === operation.object &&
  70. op.type === oppositeType &&
  71. op.id === operation.id &&
  72. op.grantee === operation.grantee &&
  73. op.privilege_type === operation.privilege_type
  74. )
  75. })
  76. if (existing !== undefined) {
  77. state = state.filter((op) => op !== existing)
  78. if (!forceAdd) {
  79. return state
  80. }
  81. }
  82. state.push(operation)
  83. return state
  84. }
  85. export function usePrivilegesState({
  86. defaultTableCheckedStates,
  87. defaultColumnCheckedStates,
  88. tableId,
  89. role,
  90. }: UsePrivilegesStateOptions) {
  91. const [operations, setOperations] = useState<PrivilegeOperation[]>([])
  92. const tableCheckedStates = operations.reduce((acc, op) => {
  93. if (op.object === 'table' && op.id === tableId && op.grantee === role) {
  94. return {
  95. ...acc,
  96. [op.privilege_type]: op.type === 'grant',
  97. }
  98. }
  99. return acc
  100. }, defaultTableCheckedStates)
  101. const columnCheckedStates = operations.reduce((acc, op) => {
  102. let curr = acc
  103. if (op.object === 'table' && op.grantee === role) {
  104. curr = Object.fromEntries(
  105. Object.entries(curr).map(([id, column]) => [
  106. id,
  107. Object.fromEntries(
  108. Object.entries(column).map(([privilege, value]) => [
  109. privilege,
  110. op.privilege_type === privilege ? op.type === 'grant' : value,
  111. ])
  112. ),
  113. ])
  114. )
  115. }
  116. if (op.object === 'column' && op.grantee === role) {
  117. return {
  118. ...curr,
  119. [op.id]: {
  120. ...curr[op.id],
  121. [op.privilege_type]: op.type === 'grant',
  122. },
  123. }
  124. }
  125. return curr
  126. }, defaultColumnCheckedStates)
  127. function toggleTablePrivilege(privilegeType: string) {
  128. const shouldGrant = !tableCheckedStates[privilegeType]
  129. setOperations((prevState) => {
  130. let state = [...prevState]
  131. if (COLUMN_PRIVILEGE_TYPES.includes(privilegeType as ColumnPrivilegeType)) {
  132. if (shouldGrant) {
  133. // remove all operations for the columns since
  134. // the table privilege will take precedence
  135. state = state.filter(
  136. (op) =>
  137. !(
  138. op.object === 'column' &&
  139. op.grantee === role &&
  140. op.privilege_type === privilegeType
  141. )
  142. )
  143. }
  144. }
  145. state = addOrRemoveOperation(state, {
  146. object: 'table',
  147. type: shouldGrant ? 'grant' : 'revoke',
  148. id: tableId,
  149. grantee: role,
  150. privilege_type: privilegeType,
  151. })
  152. return state
  153. })
  154. }
  155. function toggleColumnPrivilege(columnId: string, privilegeType: string) {
  156. const shouldGrant = !columnCheckedStates[columnId][privilegeType]
  157. setOperations((prevState) => {
  158. let state = [...prevState]
  159. // if the user is revoking a column and the table is enabled
  160. if (!shouldGrant && tableCheckedStates[privilegeType]) {
  161. // also revoke the table privilege
  162. state = addOrRemoveOperation(state, {
  163. object: 'table',
  164. type: 'revoke',
  165. id: tableId,
  166. grantee: role,
  167. privilege_type: privilegeType,
  168. })
  169. // grant all other enabled columns
  170. const operations = Object.entries(columnCheckedStates)
  171. .filter(([id]) => id !== columnId)
  172. .map(([id, column]) => ({
  173. object: 'column' as const,
  174. type: column[privilegeType] ? ('grant' as const) : ('revoke' as const),
  175. id,
  176. grantee: role,
  177. privilege_type: privilegeType,
  178. }))
  179. operations.forEach((op) => {
  180. state = addOrRemoveOperation(state, op)
  181. })
  182. }
  183. if (shouldGrant) {
  184. const areAllOtherColumnsEnabled = Object.entries(columnCheckedStates).every(
  185. ([id, column]) => id === columnId || column[privilegeType]
  186. )
  187. if (areAllOtherColumnsEnabled) {
  188. // remove all operations for the columns since
  189. // the table privilege will take precedence
  190. state = state.filter(
  191. (op) =>
  192. !(
  193. op.object === 'column' &&
  194. op.grantee === role &&
  195. op.privilege_type === privilegeType
  196. )
  197. )
  198. // grant the table privilege
  199. state = addOrRemoveOperation(state, {
  200. object: 'table',
  201. type: 'grant',
  202. id: tableId,
  203. grantee: role,
  204. privilege_type: privilegeType,
  205. })
  206. return state
  207. }
  208. }
  209. state = addOrRemoveOperation(state, {
  210. object: 'column',
  211. type: shouldGrant ? 'grant' : 'revoke',
  212. id: columnId,
  213. grantee: role,
  214. privilege_type: privilegeType,
  215. })
  216. return state
  217. })
  218. }
  219. const resetOperations = useCallback(() => {
  220. setOperations([])
  221. }, [])
  222. return {
  223. tableCheckedStates,
  224. columnCheckedStates,
  225. operations,
  226. toggleTablePrivilege,
  227. toggleColumnPrivilege,
  228. resetOperations,
  229. }
  230. }
  231. export function useApplyPrivilegeOperations(callback?: () => void) {
  232. const { data: project } = useSelectedProjectQuery()
  233. const queryClient = useQueryClient()
  234. const [isLoading, setIsLoading] = useState(false)
  235. const apply = useCallback(
  236. async (operations: PrivilegeOperation[]) => {
  237. if (!project) return console.error('No project selected')
  238. setIsLoading(true)
  239. const tableOperations = operations.filter((op) => op.object === 'table')
  240. const columnOperations = operations.filter((op) => op.object === 'column')
  241. const grantTableOperations = tableOperations
  242. .filter((op) => op.type === 'grant')
  243. .map((op) => ({
  244. relationId: Number(op.id),
  245. grantee: op.grantee,
  246. privilegeType: op.privilege_type as TablePrivilegesGrant['privilegeType'],
  247. }))
  248. const revokeTableOperations = tableOperations
  249. .filter((op) => op.type === 'revoke')
  250. .map((op) => ({
  251. relationId: Number(op.id),
  252. grantee: op.grantee,
  253. privilegeType: op.privilege_type as TablePrivilegesRevoke['privilegeType'],
  254. }))
  255. const grantColumnOperations = columnOperations
  256. .filter((op) => op.type === 'grant')
  257. .map((op) => ({
  258. column_id: String(op.id),
  259. grantee: op.grantee,
  260. privilege_type: op.privilege_type as ColumnPrivilegesRevoke['privilege_type'],
  261. }))
  262. const revokeColumnOperations = columnOperations
  263. .filter((op) => op.type === 'revoke')
  264. .map((op) => ({
  265. column_id: String(op.id),
  266. grantee: op.grantee,
  267. privilege_type: op.privilege_type as ColumnPrivilegesRevoke['privilege_type'],
  268. }))
  269. // annoyingly these can't be run all at once
  270. // as postgres can't process them in parallel
  271. if (revokeTableOperations.length > 0) {
  272. await revokeTablePrivileges({
  273. projectRef: project.ref,
  274. connectionString: project.connectionString,
  275. revokes: revokeTableOperations,
  276. })
  277. }
  278. if (grantTableOperations.length > 0) {
  279. await grantTablePrivileges({
  280. projectRef: project.ref,
  281. connectionString: project.connectionString,
  282. grants: grantTableOperations,
  283. })
  284. }
  285. if (revokeColumnOperations.length > 0) {
  286. await revokeColumnPrivileges({
  287. projectRef: project.ref,
  288. connectionString: project.connectionString,
  289. revokes: revokeColumnOperations,
  290. })
  291. }
  292. if (grantColumnOperations.length > 0) {
  293. await grantColumnPrivileges({
  294. projectRef: project.ref,
  295. connectionString: project.connectionString,
  296. grants: grantColumnOperations,
  297. })
  298. }
  299. await Promise.all([
  300. queryClient.invalidateQueries({ queryKey: privilegeKeys.tablePrivilegesList(project.ref) }),
  301. queryClient.invalidateQueries({
  302. queryKey: privilegeKeys.columnPrivilegesList(project.ref),
  303. }),
  304. ])
  305. setIsLoading(false)
  306. callback?.()
  307. },
  308. [callback, project, queryClient]
  309. )
  310. return { apply, isLoading }
  311. }