StoragePolicies.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. import { useParams } from 'common'
  2. import { isEmpty } from 'lodash'
  3. import { parseAsString, useQueryState } from 'nuqs'
  4. import { useMemo, useState } from 'react'
  5. import { toast } from 'sonner'
  6. import { GenericSkeletonLoader } from 'ui-patterns'
  7. import { ConfirmationModal } from 'ui-patterns/Dialogs/ConfirmationModal'
  8. import { PageContainer } from 'ui-patterns/PageContainer'
  9. import {
  10. PageSection,
  11. PageSectionContent,
  12. PageSectionDescription,
  13. PageSectionMeta,
  14. PageSectionSummary,
  15. PageSectionTitle,
  16. } from 'ui-patterns/PageSection'
  17. import { formatPoliciesForStorage, UNGROUPED_POLICY_SYMBOL } from '../Storage.utils'
  18. import { StoragePoliciesBucketRow } from './StoragePoliciesBucketRow'
  19. import { BucketsPolicies, type SelectBucketPolicyForAction } from './StoragePoliciesBucketsSection'
  20. import { StoragePoliciesEditPolicyModal } from './StoragePoliciesEditPolicyModal'
  21. import type {
  22. PostgresPolicyCreatePayload,
  23. PostgresPolicyUpdatePayload,
  24. } from '@/components/interfaces/Auth/Policies/Policies.types'
  25. import { PolicyEditorModal } from '@/components/interfaces/Auth/Policies/PolicyEditorModal'
  26. import type { Policy } from '@/components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRow.utils'
  27. import { useDatabasePoliciesQuery } from '@/data/database-policies/database-policies-query'
  28. import { useDatabasePolicyCreateMutation } from '@/data/database-policies/database-policy-create-mutation'
  29. import { useDatabasePolicyDeleteMutation } from '@/data/database-policies/database-policy-delete-mutation'
  30. import { useDatabasePolicyUpdateMutation } from '@/data/database-policies/database-policy-update-mutation'
  31. import { usePaginatedBucketsQuery } from '@/data/storage/buckets-query'
  32. import { useDebouncedValue } from '@/hooks/misc/useDebouncedValue'
  33. import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
  34. export const StoragePolicies = () => {
  35. const { ref: projectRef } = useParams()
  36. const { data: project } = useSelectedProjectQuery()
  37. const [selectedPolicyToEdit, setSelectedPolicyToEdit] = useState<Policy>()
  38. const [selectedPolicyToDelete, setSelectedPolicyToDelete] = useState<Policy>()
  39. const [isEditingPolicyForBucket, setIsEditingPolicyForBucket] = useState<{
  40. bucket: string
  41. table: string
  42. }>()
  43. const [searchString, setSearchString] = useQueryState(
  44. 'search',
  45. parseAsString.withDefault('').withOptions({ history: 'replace', clearOnDefault: true })
  46. )
  47. const debouncedSearchString = useDebouncedValue(searchString, 250)
  48. const {
  49. data: bucketsData,
  50. isPending: isLoadingBuckets,
  51. hasNextPage,
  52. isFetchingNextPage,
  53. fetchNextPage,
  54. } = usePaginatedBucketsQuery({
  55. projectRef,
  56. search: debouncedSearchString || undefined,
  57. })
  58. const buckets = useMemo(() => bucketsData?.pages.flatMap((page) => page) ?? [], [bucketsData])
  59. const {
  60. data: policies = [],
  61. refetch,
  62. isPending: isLoadingPolicies,
  63. } = useDatabasePoliciesQuery({
  64. projectRef: project?.ref,
  65. connectionString: project?.connectionString,
  66. schema: 'storage',
  67. })
  68. const isLoading = isLoadingBuckets || isLoadingPolicies
  69. const { mutateAsync: createDatabasePolicy } = useDatabasePolicyCreateMutation({
  70. onError: () => {},
  71. })
  72. const { mutateAsync: updateDatabasePolicy } = useDatabasePolicyUpdateMutation()
  73. const { mutate: deleteDatabasePolicy, isPending: isDeletingPolicy } =
  74. useDatabasePolicyDeleteMutation({
  75. onSuccess: async () => {
  76. await refetch()
  77. toast.success('Successfully deleted policy!')
  78. setSelectedPolicyToDelete(undefined)
  79. },
  80. onError: (error: any) => {
  81. toast.error(`Failed to delete policy: ${error.message}`)
  82. },
  83. })
  84. // Only use storage policy editor when creating new policies for buckets
  85. const showStoragePolicyEditor =
  86. isEmpty(selectedPolicyToEdit) &&
  87. !isEmpty(isEditingPolicyForBucket) &&
  88. (isEditingPolicyForBucket.bucket ?? '').length > 0
  89. const showGeneralPolicyEditor = !isEmpty(isEditingPolicyForBucket) && !showStoragePolicyEditor
  90. // Policies under storage.buckets
  91. const storageBucketPolicies = policies.filter(
  92. (x) => x.schema === 'storage' && x.table === 'buckets'
  93. )
  94. // Policies under storage.objects
  95. const storageObjectsPolicies = policies.filter(
  96. (x) => x.schema === 'storage' && x.table === 'objects'
  97. )
  98. const formattedStorageObjectPolicies = useMemo(
  99. () => formatPoliciesForStorage(buckets, storageObjectsPolicies),
  100. [buckets, storageObjectsPolicies]
  101. )
  102. const ungroupedPolicies =
  103. formattedStorageObjectPolicies.find((x) => x.name === UNGROUPED_POLICY_SYMBOL)?.policies ?? []
  104. const bucketsWithPolicies = useMemo(() => {
  105. // Get policies for filtered buckets (show all policies, don't filter them)
  106. // Show all filtered buckets, even if they don't have policies
  107. return buckets.map((bucket) => {
  108. const bucketPolicies =
  109. formattedStorageObjectPolicies.find((x) => x.name === bucket.name)?.policies ?? []
  110. return { bucket, policies: bucketPolicies }
  111. })
  112. }, [buckets, formattedStorageObjectPolicies])
  113. const onSelectPolicyAdd: SelectBucketPolicyForAction['addPolicy'] = (
  114. bucketName = '',
  115. table = ''
  116. ) => {
  117. setSelectedPolicyToEdit(undefined)
  118. setIsEditingPolicyForBucket({ bucket: bucketName, table })
  119. }
  120. const onSelectPolicyEdit: SelectBucketPolicyForAction['editPolicy'] = (
  121. policy,
  122. bucketName = '',
  123. table = ''
  124. ) => {
  125. setIsEditingPolicyForBucket({ bucket: bucketName, table })
  126. setSelectedPolicyToEdit(policy)
  127. }
  128. const onCancelPolicyEdit = () => {
  129. setIsEditingPolicyForBucket(undefined)
  130. }
  131. const onSelectPolicyDelete: SelectBucketPolicyForAction['deletePolicy'] = (policy: any) =>
  132. setSelectedPolicyToDelete(policy)
  133. const onCancelPolicyDelete = () => setSelectedPolicyToDelete(undefined)
  134. const onSavePolicySuccess = async () => {
  135. toast.success('Successfully saved policy!')
  136. await refetch()
  137. onCancelPolicyEdit()
  138. }
  139. /*
  140. Functions that involve the CRUD for policies
  141. For each API call within the Promise.all, return true if an error occurred, else return false
  142. */
  143. const onCreatePolicies = async (payloads: PostgresPolicyCreatePayload[]) => {
  144. if (!project) {
  145. console.error('Project is required')
  146. return true
  147. }
  148. try {
  149. return await Promise.all(
  150. payloads.map(async (payload) => {
  151. try {
  152. await createDatabasePolicy({
  153. projectRef: project?.ref,
  154. connectionString: project?.connectionString,
  155. payload,
  156. })
  157. return false
  158. } catch (error: any) {
  159. toast.error(`Error adding policy: ${error.message}`)
  160. return true
  161. }
  162. })
  163. )
  164. } finally {
  165. }
  166. }
  167. const onCreatePolicy = async (payload: PostgresPolicyCreatePayload) => {
  168. if (!project) {
  169. console.error('Project is required')
  170. return true
  171. }
  172. try {
  173. await createDatabasePolicy({
  174. projectRef: project?.ref,
  175. connectionString: project?.connectionString,
  176. payload,
  177. })
  178. return false
  179. } catch (error: any) {
  180. toast.error(`Error adding policy: ${error.message}`)
  181. return true
  182. }
  183. }
  184. const onUpdatePolicy = async (payload: PostgresPolicyUpdatePayload) => {
  185. if (!project) {
  186. console.error('Project is required')
  187. return true
  188. }
  189. if (!selectedPolicyToEdit) {
  190. console.error('Unable to find policy')
  191. return true
  192. }
  193. try {
  194. await updateDatabasePolicy({
  195. projectRef: project?.ref,
  196. connectionString: project?.connectionString,
  197. originalPolicy: selectedPolicyToEdit,
  198. payload,
  199. })
  200. return false
  201. } catch (error: any) {
  202. toast.error(`Error updating policy: ${error.message}`)
  203. return true
  204. }
  205. }
  206. const onDeletePolicy = async () => {
  207. if (!project) return console.error('Project is required')
  208. if (!selectedPolicyToDelete) return console.error('Unable to find policy')
  209. deleteDatabasePolicy({
  210. projectRef: project?.ref,
  211. connectionString: project?.connectionString,
  212. originalPolicy: selectedPolicyToDelete,
  213. })
  214. }
  215. return (
  216. <>
  217. <PageContainer>
  218. {isLoading ? (
  219. <PageSection>
  220. <PageSectionContent>
  221. <GenericSkeletonLoader />
  222. </PageSectionContent>
  223. </PageSection>
  224. ) : (
  225. <div>
  226. <BucketsPolicies
  227. buckets={bucketsWithPolicies}
  228. search={searchString}
  229. debouncedSearch={debouncedSearchString}
  230. setSearch={setSearchString}
  231. actions={{
  232. addPolicy: onSelectPolicyAdd,
  233. editPolicy: onSelectPolicyEdit,
  234. deletePolicy: onSelectPolicyDelete,
  235. }}
  236. pagination={{
  237. hasNextPage,
  238. isFetchingNextPage,
  239. fetchNextPage,
  240. }}
  241. />
  242. <PageSection>
  243. <PageSectionMeta>
  244. <PageSectionSummary>
  245. <PageSectionTitle>Schema</PageSectionTitle>
  246. <PageSectionDescription>
  247. Write policies for the tables under the storage schema directly for greater
  248. control
  249. </PageSectionDescription>
  250. </PageSectionSummary>
  251. </PageSectionMeta>
  252. <PageSectionContent>
  253. <div className="flex flex-col gap-y-4">
  254. {/* Section for policies under storage.objects that are not tied to any buckets */}
  255. <StoragePoliciesBucketRow
  256. table="objects"
  257. label="Other policies under storage.objects"
  258. policies={ungroupedPolicies}
  259. onSelectPolicyAdd={onSelectPolicyAdd}
  260. onSelectPolicyEdit={onSelectPolicyEdit}
  261. onSelectPolicyDelete={onSelectPolicyDelete}
  262. />
  263. {/* Section for policies under storage.buckets */}
  264. <StoragePoliciesBucketRow
  265. table="buckets"
  266. label="Policies under storage.buckets"
  267. policies={storageBucketPolicies}
  268. onSelectPolicyAdd={onSelectPolicyAdd}
  269. onSelectPolicyEdit={onSelectPolicyEdit}
  270. onSelectPolicyDelete={onSelectPolicyDelete}
  271. />
  272. </div>
  273. </PageSectionContent>
  274. </PageSection>
  275. </div>
  276. )}
  277. </PageContainer>
  278. {/* Only used for adding policies to buckets */}
  279. <StoragePoliciesEditPolicyModal
  280. visible={showStoragePolicyEditor}
  281. bucketName={isEditingPolicyForBucket?.bucket}
  282. onSelectCancel={onCancelPolicyEdit}
  283. onCreatePolicies={onCreatePolicies}
  284. onSaveSuccess={onSavePolicySuccess}
  285. />
  286. {/* Adding policies to objets/buckets table or editting any policy uses the general policy editor */}
  287. <PolicyEditorModal
  288. schema="storage"
  289. visible={showGeneralPolicyEditor}
  290. table={isEditingPolicyForBucket?.table ?? ''}
  291. selectedPolicyToEdit={selectedPolicyToEdit}
  292. onSelectCancel={onCancelPolicyEdit}
  293. onCreatePolicy={onCreatePolicy}
  294. onUpdatePolicy={onUpdatePolicy}
  295. onSaveSuccess={onSavePolicySuccess}
  296. />
  297. <ConfirmationModal
  298. visible={!isEmpty(selectedPolicyToDelete)}
  299. variant="destructive"
  300. title="Delete policy"
  301. description={`Are you sure you want to delete the policy “${selectedPolicyToDelete?.name}”? This action cannot be undone.`}
  302. confirmLabel="Delete"
  303. confirmLabelLoading="Deleting"
  304. loading={isDeletingPolicy}
  305. onCancel={onCancelPolicyDelete}
  306. onConfirm={onDeletePolicy}
  307. />
  308. </>
  309. )
  310. }