Storage.utils.ts 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. import { difference } from 'lodash'
  2. import { useRouter } from 'next/router'
  3. import { STORAGE_CLIENT_LIBRARY_MAPPINGS } from './Storage.constants'
  4. import type { StoragePolicyFormField } from './Storage.types'
  5. import type { Policy } from '@/components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRow.utils'
  6. import { WrapperMeta } from '@/components/interfaces/Integrations/Wrappers/Wrappers.types'
  7. import { convertKVStringArrayToJson } from '@/components/interfaces/Integrations/Wrappers/Wrappers.utils'
  8. import { FDW } from '@/data/fdw/fdws-query'
  9. import { Bucket } from '@/data/storage/buckets-query'
  10. import { getDecryptedValues } from '@/data/vault/vault-secret-decrypted-value-query'
  11. import { createWrappedSymbol } from '@/lib/helpers'
  12. const shortHash = (str: string) => {
  13. let hash = 0
  14. for (let i = 0; i < str.length; i++) {
  15. const char = str.charCodeAt(i)
  16. hash = (hash << 5) - hash + char
  17. hash &= hash // Convert to 32bit integer
  18. }
  19. return new Uint32Array([hash])[0].toString(36)
  20. }
  21. export type PoliciesByBucket = { name: string | Symbol; policies: Policy[] }[]
  22. /**
  23. * Formats the policies from the objects table in the storage schema
  24. * to be consumable for the storage policies dashboard.
  25. *
  26. * @param policies All policies from a table in a schema
  27. */
  28. export const formatPoliciesForStorage = (
  29. buckets: Bucket[],
  30. policies: Policy[]
  31. ): PoliciesByBucket => {
  32. if (policies.length === 0) return []
  33. /**
  34. * Format policies from storage objects to:
  35. * - Include bucket name
  36. * - Strip away ${bucketName}_{idx} suffix
  37. * - Strip away bucket_id from definitions
  38. * Note, if the policy definition has no bucket_id, we skip the formatting
  39. */
  40. const formattedPolicies = formatStoragePolicies(buckets, policies)
  41. const policiesByBucket = groupPoliciesByBucket(formattedPolicies)
  42. return policiesByBucket
  43. }
  44. /**
  45. * Policy that belongs to a bucket which is not loaded yet (might not have been
  46. * paginated to yet, or might have been deleted)
  47. */
  48. export const UNKNOWN_BUCKET_SYMBOL = createWrappedSymbol('unknown-bucket', 'Unknown')
  49. /**
  50. * Policy that is not associated with a specific bucket
  51. */
  52. export const UNGROUPED_POLICY_SYMBOL = createWrappedSymbol('ungrouped-policy', 'Ungrouped')
  53. const formatStoragePolicies = (buckets: Bucket[], policies: Policy[]) => {
  54. const availableBuckets = buckets.map((bucket) => bucket.name)
  55. const formattedPolicies = policies.map((policy) => {
  56. const { definition: policyDefinition, check: policyCheck } = policy
  57. const bucketName =
  58. policyDefinition !== null
  59. ? extractBucketNameFromDefinition(policyDefinition)
  60. : extractBucketNameFromDefinition(policyCheck)
  61. if (bucketName) {
  62. const isBucketLoaded = availableBuckets.includes(bucketName)
  63. return {
  64. ...policy,
  65. bucket: isBucketLoaded ? bucketName : UNKNOWN_BUCKET_SYMBOL,
  66. }
  67. }
  68. return { ...policy, bucket: UNGROUPED_POLICY_SYMBOL }
  69. })
  70. return formattedPolicies
  71. }
  72. export const extractBucketNameFromDefinition = (definition: string | null) => {
  73. if (!definition) return null
  74. const definitionSegments = definition?.split(' AND ') ?? []
  75. const [bucketDefinition] = definitionSegments.filter((segment: string) =>
  76. segment.includes('bucket_id')
  77. )
  78. return bucketDefinition ? bucketDefinition.split("'")[1] : null
  79. }
  80. const groupPoliciesByBucket = (policies: (Policy & { bucket: string | Symbol })[]) => {
  81. const policiesByBucket = new Map<string | Symbol, Policy[]>()
  82. policies.forEach((policy) => {
  83. if (!policiesByBucket.has(policy.bucket)) {
  84. policiesByBucket.set(policy.bucket, [])
  85. }
  86. policiesByBucket.get(policy.bucket)?.push(policy)
  87. })
  88. return Array.from(policiesByBucket).map(([bucketName, policies]) => ({
  89. name: bucketName,
  90. policies,
  91. }))
  92. }
  93. export const createPayloadsForAddPolicy = (
  94. bucketName = '',
  95. policyFormFields: StoragePolicyFormField,
  96. addSuffixToPolicyName = true
  97. ) => {
  98. const { name: policyName, definition, allowedOperations, roles } = policyFormFields
  99. const formattedDefinition = definition ? definition.replace(/\s+/g, ' ').trim() : ''
  100. return allowedOperations.map((operation: any, idx: number) => {
  101. return createPayloadForNewPolicy(
  102. idx,
  103. bucketName,
  104. policyName,
  105. formattedDefinition,
  106. operation,
  107. roles,
  108. addSuffixToPolicyName
  109. )
  110. })
  111. }
  112. const createPayloadForNewPolicy = (
  113. idx: number,
  114. bucketName: string,
  115. policyName: string,
  116. definition: string,
  117. operation: string,
  118. roles: string[],
  119. addSuffixToPolicyName: boolean
  120. ) => {
  121. const hashedBucketName = shortHash(bucketName)
  122. return {
  123. name: addSuffixToPolicyName ? `${policyName} ${hashedBucketName}_${idx}` : policyName,
  124. definition: operation === 'INSERT' ? undefined : `(${definition})`,
  125. action: 'PERMISSIVE',
  126. check: operation === 'INSERT' ? `(${definition})` : undefined,
  127. command: operation,
  128. schema: 'storage',
  129. table: 'objects',
  130. roles: roles.length > 0 ? roles : undefined,
  131. }
  132. }
  133. // Used in the policy editor to highlight which library methods are allowed depending on which operations are allowed
  134. export const deriveAllowedClientLibraryMethods = (allowedOperations = []) => {
  135. return Object.keys(STORAGE_CLIENT_LIBRARY_MAPPINGS).filter((method) => {
  136. const requiredOperations = (STORAGE_CLIENT_LIBRARY_MAPPINGS as any)[method]
  137. if (difference(requiredOperations, allowedOperations).length === 0) {
  138. return method
  139. }
  140. })
  141. }
  142. // Create policy SQL statements on save based on configuration.
  143. // Used purely for previewing in the review step, not actually fired
  144. const createSQLStatementForCreatePolicy = (
  145. idx: number,
  146. bucketName: string,
  147. policyName: string,
  148. definition: string,
  149. operation: string,
  150. selectedRoles: string[],
  151. addSuffixToPolicyName: boolean
  152. ) => {
  153. const hashedBucketName = shortHash(bucketName)
  154. const formattedPolicyName = addSuffixToPolicyName
  155. ? `${policyName} ${hashedBucketName}_${idx}`
  156. : policyName
  157. const description = `Add policy for the ${operation} operation under the policy "${policyName}"`
  158. const roles = selectedRoles.length === 0 ? ['public'] : selectedRoles
  159. const statement = `
  160. CREATE POLICY "${formattedPolicyName}"
  161. ON storage.objects
  162. FOR ${operation}
  163. TO ${roles.join(', ')}
  164. ${operation === 'INSERT' ? 'WITH CHECK' : 'USING'} (${definition});
  165. `
  166. .replace(/\s+/g, ' ')
  167. .trim()
  168. return { description, statement }
  169. }
  170. export const createSQLPolicies = (
  171. bucketName: string,
  172. policyFormFields: StoragePolicyFormField,
  173. addSuffixToPolicyName = true
  174. ) => {
  175. const { name: policyName, definition, allowedOperations, roles } = policyFormFields
  176. const policies = allowedOperations.map((operation: any, idx: number) =>
  177. createSQLStatementForCreatePolicy(
  178. idx,
  179. bucketName,
  180. policyName,
  181. definition || '',
  182. operation,
  183. roles,
  184. addSuffixToPolicyName
  185. )
  186. )
  187. return policies
  188. }
  189. export const applyBucketIdToTemplateDefinition = (definition: string, bucketId: any) => {
  190. return definition.replace('{bucket_id}', `'${bucketId}'`)
  191. }
  192. export const useStorageV2Page = () => {
  193. const router = useRouter()
  194. return router.pathname.split('/')[4] as undefined | 'files' | 'analytics' | 'vectors' | 's3'
  195. }
  196. export const getDecryptedParameters = async ({
  197. ref,
  198. connectionString,
  199. wrapper,
  200. wrapperMeta,
  201. }: {
  202. ref?: string
  203. connectionString?: string
  204. wrapper: FDW
  205. wrapperMeta: WrapperMeta
  206. }) => {
  207. const wrapperServerOptions = wrapperMeta.server.options
  208. const serverOptions = convertKVStringArrayToJson(wrapper?.server_options ?? [])
  209. const paramsToBeDecrypted = Object.fromEntries(
  210. new Map(
  211. Object.entries(serverOptions).filter(([key, _value]) => {
  212. return wrapperServerOptions.find((option) => option.name === key)?.encrypted
  213. })
  214. )
  215. )
  216. const decryptedValues = await getDecryptedValues({
  217. projectRef: ref,
  218. connectionString: connectionString,
  219. ids: Object.values(paramsToBeDecrypted),
  220. })
  221. const paramsWithDecryptedValues = Object.fromEntries(
  222. new Map(
  223. Object.entries(paramsToBeDecrypted).map(([name, id]) => {
  224. const decryptedValue = decryptedValues[id]
  225. return [name, decryptedValue]
  226. })
  227. )
  228. )
  229. return {
  230. ...serverOptions,
  231. ...paramsWithDecryptedValues,
  232. }
  233. }