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