mirror of
https://github.com/supabase/supabase.git
synced 2026-09-07 02:20:52 +08:00
## Context Previous PR was [here](https://github.com/supabase/supabase/pull/45143) but it got stale with lots of conflicts so figured it'll be easier redo it off the latest master Moves policies page from Auth to Database under an Access Control section along with Roles. This moves all existing files, applies redirects, and updates urls to point to the new route <img width="274" height="412" alt="image" src="https://github.com/user-attachments/assets/7952c185-64ae-4355-ba36-45397efe1787" /> <img width="453" height="471" alt="image" src="https://github.com/user-attachments/assets/04b3dcb3-48a5-4049-9893-d01109fb46a9" /> ## To test - [ ] Verify that policies now live under Database correctly <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a quick navigation shortcut to open **Database > Policies (RLS)**. * **Bug Fixes** * Updated Policies and RLS-related links across the product to open the **Database policies** area (menus, command palette, context actions, alerts, and link-outs). * Added a permanent redirect from the old **auth policies** URL to the new **database policies** URL. * **Documentation** * Updated RLS Dashboard and security checklist instructions to reference **Database > Policies**. * **Tests** * Adjusted automated tests to validate the new Policies route. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
265 lines
8.1 KiB
TypeScript
265 lines
8.1 KiB
TypeScript
import { difference } from 'lodash'
|
|
import { useRouter } from 'next/router'
|
|
|
|
import { STORAGE_CLIENT_LIBRARY_MAPPINGS } from './Storage.constants'
|
|
import type { StoragePolicyFormField } from './Storage.types'
|
|
import type { Policy } from '@/components/interfaces/Database/Policies/PolicyTableRow/PolicyTableRow.utils'
|
|
import { WrapperMeta } from '@/components/interfaces/Integrations/Wrappers/Wrappers.types'
|
|
import { convertKVStringArrayToJson } from '@/components/interfaces/Integrations/Wrappers/Wrappers.utils'
|
|
import { FDW } from '@/data/fdw/fdws-query'
|
|
import { Bucket } from '@/data/storage/buckets-query'
|
|
import { getDecryptedValues } from '@/data/vault/vault-secret-decrypted-value-query'
|
|
import { createWrappedSymbol } from '@/lib/helpers'
|
|
|
|
const shortHash = (str: string) => {
|
|
let hash = 0
|
|
for (let i = 0; i < str.length; i++) {
|
|
const char = str.charCodeAt(i)
|
|
hash = (hash << 5) - hash + char
|
|
hash &= hash // Convert to 32bit integer
|
|
}
|
|
return new Uint32Array([hash])[0].toString(36)
|
|
}
|
|
|
|
export type PoliciesByBucket = { name: string | Symbol; policies: Policy[] }[]
|
|
|
|
/**
|
|
* Formats the policies from the objects table in the storage schema
|
|
* to be consumable for the storage policies dashboard.
|
|
*
|
|
* @param policies All policies from a table in a schema
|
|
*/
|
|
export const formatPoliciesForStorage = (
|
|
buckets: Bucket[],
|
|
policies: Policy[]
|
|
): PoliciesByBucket => {
|
|
if (policies.length === 0) return []
|
|
|
|
/**
|
|
* Format policies from storage objects to:
|
|
* - Include bucket name
|
|
* - Strip away ${bucketName}_{idx} suffix
|
|
* - Strip away bucket_id from definitions
|
|
* Note, if the policy definition has no bucket_id, we skip the formatting
|
|
*/
|
|
const formattedPolicies = formatStoragePolicies(buckets, policies)
|
|
|
|
const policiesByBucket = groupPoliciesByBucket(formattedPolicies)
|
|
return policiesByBucket
|
|
}
|
|
|
|
/**
|
|
* Policy that belongs to a bucket which is not loaded yet (might not have been
|
|
* paginated to yet, or might have been deleted)
|
|
*/
|
|
export const UNKNOWN_BUCKET_SYMBOL = createWrappedSymbol('unknown-bucket', 'Unknown')
|
|
/**
|
|
* Policy that is not associated with a specific bucket
|
|
*/
|
|
export const UNGROUPED_POLICY_SYMBOL = createWrappedSymbol('ungrouped-policy', 'Ungrouped')
|
|
|
|
const formatStoragePolicies = (buckets: Bucket[], policies: Policy[]) => {
|
|
const availableBuckets = buckets.map((bucket) => bucket.name)
|
|
const formattedPolicies = policies.map((policy) => {
|
|
const { definition: policyDefinition, check: policyCheck } = policy
|
|
|
|
const bucketName =
|
|
policyDefinition !== null
|
|
? extractBucketNameFromDefinition(policyDefinition)
|
|
: extractBucketNameFromDefinition(policyCheck)
|
|
|
|
if (bucketName) {
|
|
const isBucketLoaded = availableBuckets.includes(bucketName)
|
|
|
|
return {
|
|
...policy,
|
|
bucket: isBucketLoaded ? bucketName : UNKNOWN_BUCKET_SYMBOL,
|
|
}
|
|
}
|
|
|
|
return { ...policy, bucket: UNGROUPED_POLICY_SYMBOL }
|
|
})
|
|
|
|
return formattedPolicies
|
|
}
|
|
|
|
export const extractBucketNameFromDefinition = (definition: string | null) => {
|
|
if (!definition) return null
|
|
|
|
const definitionSegments = definition?.split(' AND ') ?? []
|
|
const [bucketDefinition] = definitionSegments.filter((segment: string) =>
|
|
segment.includes('bucket_id')
|
|
)
|
|
return bucketDefinition ? bucketDefinition.split("'")[1] : null
|
|
}
|
|
|
|
const groupPoliciesByBucket = (policies: (Policy & { bucket: string | Symbol })[]) => {
|
|
const policiesByBucket = new Map<string | Symbol, Policy[]>()
|
|
policies.forEach((policy) => {
|
|
if (!policiesByBucket.has(policy.bucket)) {
|
|
policiesByBucket.set(policy.bucket, [])
|
|
}
|
|
policiesByBucket.get(policy.bucket)?.push(policy)
|
|
})
|
|
return Array.from(policiesByBucket).map(([bucketName, policies]) => ({
|
|
name: bucketName,
|
|
policies,
|
|
}))
|
|
}
|
|
|
|
export const createPayloadsForAddPolicy = (
|
|
bucketName = '',
|
|
policyFormFields: StoragePolicyFormField,
|
|
addSuffixToPolicyName = true
|
|
) => {
|
|
const { name: policyName, definition, allowedOperations, roles } = policyFormFields
|
|
const formattedDefinition = definition ? definition.replace(/\s+/g, ' ').trim() : ''
|
|
|
|
return allowedOperations.map((operation: any, idx: number) => {
|
|
return createPayloadForNewPolicy(
|
|
idx,
|
|
bucketName,
|
|
policyName,
|
|
formattedDefinition,
|
|
operation,
|
|
roles,
|
|
addSuffixToPolicyName
|
|
)
|
|
})
|
|
}
|
|
|
|
const createPayloadForNewPolicy = (
|
|
idx: number,
|
|
bucketName: string,
|
|
policyName: string,
|
|
definition: string,
|
|
operation: string,
|
|
roles: string[],
|
|
addSuffixToPolicyName: boolean
|
|
) => {
|
|
const hashedBucketName = shortHash(bucketName)
|
|
return {
|
|
name: addSuffixToPolicyName ? `${policyName} ${hashedBucketName}_${idx}` : policyName,
|
|
definition: operation === 'INSERT' ? undefined : `(${definition})`,
|
|
action: 'PERMISSIVE',
|
|
check: operation === 'INSERT' ? `(${definition})` : undefined,
|
|
command: operation,
|
|
schema: 'storage',
|
|
table: 'objects',
|
|
roles: roles.length > 0 ? roles : undefined,
|
|
}
|
|
}
|
|
|
|
// Used in the policy editor to highlight which library methods are allowed depending on which operations are allowed
|
|
export const deriveAllowedClientLibraryMethods = (allowedOperations = []) => {
|
|
return Object.keys(STORAGE_CLIENT_LIBRARY_MAPPINGS).filter((method) => {
|
|
const requiredOperations = (STORAGE_CLIENT_LIBRARY_MAPPINGS as any)[method]
|
|
if (difference(requiredOperations, allowedOperations).length === 0) {
|
|
return method
|
|
}
|
|
})
|
|
}
|
|
|
|
// Create policy SQL statements on save based on configuration.
|
|
// Used purely for previewing in the review step, not actually fired
|
|
const createSQLStatementForCreatePolicy = (
|
|
idx: number,
|
|
bucketName: string,
|
|
policyName: string,
|
|
definition: string,
|
|
operation: string,
|
|
selectedRoles: string[],
|
|
addSuffixToPolicyName: boolean
|
|
) => {
|
|
const hashedBucketName = shortHash(bucketName)
|
|
const formattedPolicyName = addSuffixToPolicyName
|
|
? `${policyName} ${hashedBucketName}_${idx}`
|
|
: policyName
|
|
const description = `Add policy for the ${operation} operation under the policy "${policyName}"`
|
|
const roles = selectedRoles.length === 0 ? ['public'] : selectedRoles
|
|
|
|
const statement = `
|
|
CREATE POLICY "${formattedPolicyName}"
|
|
ON storage.objects
|
|
FOR ${operation}
|
|
TO ${roles.join(', ')}
|
|
${operation === 'INSERT' ? 'WITH CHECK' : 'USING'} (${definition});
|
|
`
|
|
.replace(/\s+/g, ' ')
|
|
.trim()
|
|
return { description, statement }
|
|
}
|
|
|
|
export const createSQLPolicies = (
|
|
bucketName: string,
|
|
policyFormFields: StoragePolicyFormField,
|
|
addSuffixToPolicyName = true
|
|
) => {
|
|
const { name: policyName, definition, allowedOperations, roles } = policyFormFields
|
|
const policies = allowedOperations.map((operation: any, idx: number) =>
|
|
createSQLStatementForCreatePolicy(
|
|
idx,
|
|
bucketName,
|
|
policyName,
|
|
definition || '',
|
|
operation,
|
|
roles,
|
|
addSuffixToPolicyName
|
|
)
|
|
)
|
|
return policies
|
|
}
|
|
|
|
export const applyBucketIdToTemplateDefinition = (definition: string, bucketId: any) => {
|
|
return definition.replace('{bucket_id}', `'${bucketId}'`)
|
|
}
|
|
|
|
export const useStorageV2Page = () => {
|
|
const router = useRouter()
|
|
return router.pathname.split('/')[4] as undefined | 'files' | 'analytics' | 'vectors' | 's3'
|
|
}
|
|
|
|
export const getDecryptedParameters = async ({
|
|
ref,
|
|
connectionString,
|
|
wrapper,
|
|
wrapperMeta,
|
|
}: {
|
|
ref?: string
|
|
connectionString?: string
|
|
wrapper: FDW
|
|
wrapperMeta: WrapperMeta
|
|
}) => {
|
|
const wrapperServerOptions = wrapperMeta.server.options
|
|
|
|
const serverOptions = convertKVStringArrayToJson(wrapper?.server_options ?? [])
|
|
|
|
const paramsToBeDecrypted = Object.fromEntries(
|
|
new Map(
|
|
Object.entries(serverOptions).filter(([key, _value]) => {
|
|
return wrapperServerOptions.find((option) => option.name === key)?.encrypted
|
|
})
|
|
)
|
|
)
|
|
|
|
const decryptedValues = await getDecryptedValues({
|
|
projectRef: ref,
|
|
connectionString: connectionString,
|
|
ids: Object.values(paramsToBeDecrypted),
|
|
})
|
|
|
|
const paramsWithDecryptedValues = Object.fromEntries(
|
|
new Map(
|
|
Object.entries(paramsToBeDecrypted).map(([name, id]) => {
|
|
const decryptedValue = decryptedValues[id]
|
|
return [name, decryptedValue]
|
|
})
|
|
)
|
|
)
|
|
|
|
return {
|
|
...serverOptions,
|
|
...paramsWithDecryptedValues,
|
|
}
|
|
}
|