Files
supabase/apps/studio/csp.ts
Joshen Lim 9dc3998fa0 RLS Tester sandbox environment (#45839)
## Context

Resolves FE-3221

Heavily inspired by what @filipecabaco has done previously here:
https://github.com/supabase/supabase/pull/45360

This PR explores the use of pglite to set up a sandbox for RLS testing,
which will pave the way for testing mutation based queries so to ensure
no disruption to the actual database. Sandbox can be set up within the
RLS tester panel as such:
<img width="500" alt="image"
src="https://github.com/user-attachments/assets/0cfdf8e4-dd99-4dee-ac00-39a32b375c07"
/>

Which the sandbox will mimic the project's database to the bare minimum
required
- entities from the `public` schema are copied over (types, tables,
functions, policies)
- `auth` schema is pseudo setup with `SANDBOX_SETUP_STATEMENTS`
- Enough to support role impersonation + querying tables with references
to the auth schema (e.g users table)
- data is seeded up to 100 rows for each table
- More info RE limitations in the last section below

Once sandbox is ready, you'll see this UI where you can either leave the
sandbox, or re-sync the sandbox from the actual database
<img width="500" alt="image"
src="https://github.com/user-attachments/assets/d07ce55f-5bc8-4722-8ce9-898b9b458f9b"
/>

Changes are currently feature flagged, so won't be available publicly
just yet until things are ironed out and ready

## To test
- [ ] Verify that setting up sandbox works
- [ ] Verify that you can query your sandbox, and queries do not touch
the actual database (can verify that we're not sending HTTP requests to
the /query endpoint)
- [ ] Verify correctness of RLS tester as well, should match correctness
with testing against actual DB
- [ ] Verify that re-syncing sandbox picks up changes
- Can test by updating your policies that will affect the output of your
select query
  - e.g SELECT for `authenticated`, change from just `true` to `false`
- [ ] RLS tester should work as per normal (against actual DB) with the
feature flag off with no additional overhead

Let me know of any edge cases you might run into while testing

## Known quirks that will be addressed subsequently
Leaving these for now just to not bloat this PR further
- Pglite schema needs to be re-synced if updating RLS policies while
testing, to ensure that pglite gets the updated policies. Will think
about how to make this more seamless
- Sandbox has its own limitations, will need to add a dialog to inform
users how the sandbox works and what limitations to note of
- e.g only the auth schema is mimicked - so policies that reference
storage helpers won't work (although i think auth is probably the main
use case and the rest might be niche)
  - We can slowly expand tho where required
- Eventually we'll also move forward with figuring out testing mutation
queries with this sandbox

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* RLS tester gains an isolated Postgres sandbox with schema/seed import,
start/refresh/exit controls, and pre-populated auth data.
* Sandbox management UI with setup, loading, active, and error states;
refresh and destroy actions.

* **Bug Fixes**
* Role impersonation now keeps the PostgREST role set to anon while the
tester sheet is open.

* **Chores**
* Content Security Policy updated to allow sandbox/connectivity
endpoints.

* **Style**
  * Minor sheet styling adjustment (top border).

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/45839)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-18 16:22:12 +07:00

240 lines
7.5 KiB
TypeScript

const API_URL = process.env.NEXT_PUBLIC_API_URL
? new URL(process.env.NEXT_PUBLIC_API_URL).origin
: ''
const SUPABASE_URL = process.env.SUPABASE_URL ? new URL(process.env.SUPABASE_URL).origin : ''
const GOTRUE_URL = process.env.NEXT_PUBLIC_GOTRUE_URL
? new URL(process.env.NEXT_PUBLIC_GOTRUE_URL).origin
: ''
const MARKETPLACE_API_URL = process.env.NEXT_PUBLIC_MARKETPLACE_API_URL
? new URL(process.env.NEXT_PUBLIC_MARKETPLACE_API_URL).origin
: ''
const SUPABASE_PROJECTS_URL = 'https://*.supabase.co https://*.storage.supabase.co'
const SUPABASE_PROJECTS_URL_WS = 'wss://*.supabase.co'
// construct the URL for the Websocket Local URLs
let SUPABASE_LOCAL_PROJECTS_URL_WS = ''
if (SUPABASE_URL) {
const url = new URL(SUPABASE_URL)
const wsUrl = `${url.hostname}:${url.port}`
SUPABASE_LOCAL_PROJECTS_URL_WS = `ws://${wsUrl} wss://${wsUrl}`
}
// Needed to test docs search in local dev
const SUPABASE_DOCS_PROJECT_URL = process.env.NEXT_PUBLIC_SUPABASE_URL
? new URL(process.env.NEXT_PUBLIC_SUPABASE_URL).origin
: ''
// Needed to test docs content API in local dev
const SUPABASE_CONTENT_API_URL = process.env.NEXT_PUBLIC_CONTENT_API_URL
? new URL(process.env.NEXT_PUBLIC_CONTENT_API_URL).origin
: ''
const isDevOrStaging =
process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview' ||
process.env.NEXT_PUBLIC_ENVIRONMENT === 'local' ||
process.env.NEXT_PUBLIC_ENVIRONMENT === 'staging'
const NIMBUS_STAGING_PROJECTS_URL = 'https://*.nmb-proj.com'
const NIMBUS_STAGING_PROJECTS_URL_WS = 'wss://*.nmb-proj.com'
const NIMBUS_PROD_PROJECTS_URL = process.env.NIMBUS_PROD_PROJECTS_URL || ''
const NIMBUS_PROD_PROJECTS_URL_WS = process.env.NIMBUS_PROD_PROJECTS_URL_WS || ''
const SUPABASE_STAGING_PROJECTS_URL = 'https://*.supabase.red https://*.storage.supabase.red'
const SUPABASE_STAGING_PROJECTS_URL_WS = 'wss://*.supabase.red'
const SUPABASE_COM_URL = 'https://supabase.com'
const CLOUDFLARE_CDN_URL = 'https://cdnjs.cloudflare.com'
const HCAPTCHA_SUBDOMAINS_URL = 'https://*.hcaptcha.com'
const HCAPTCHA_ASSET_URL = 'https://newassets.hcaptcha.com'
const HCAPTCHA_JS_URL = 'https://js.hcaptcha.com'
const CONFIGCAT_URL = 'https://cdn-global.configcat.com'
const CONFIGCAT_PROXY_URL = ['staging', 'local'].includes(process.env.NEXT_PUBLIC_ENVIRONMENT ?? '')
? 'https://configcat.supabase.green'
: 'https://configcat.supabase.com'
const STRIPE_SUBDOMAINS_URL = 'https://*.stripe.com'
const STRIPE_JS_URL = 'https://js.stripe.com'
const STRIPE_NETWORK_URL = 'https://*.stripe.network'
const CLOUDFLARE_URL = 'https://www.cloudflare.com'
const VERCEL_URL = 'https://vercel.com'
const VERCEL_INSIGHTS_URL = 'https://*.vercel-insights.com'
const GITHUB_API_URL = 'https://api.github.com'
const GITHUB_USER_CONTENT_URL = 'https://raw.githubusercontent.com'
const GITHUB_USER_AVATAR_URL = 'https://avatars.githubusercontent.com'
const GOOGLE_USER_AVATAR_URL = 'https://lh3.googleusercontent.com'
// This is a custom domain for Stape, which isused for GTM servers
const STAPE_URL = 'https://ss.supabase.com'
const VERCEL_LIVE_URL = 'https://vercel.live'
const SENTRY_URL =
'https://*.ingest.sentry.io https://*.ingest.us.sentry.io https://*.ingest.de.sentry.io'
const SUPABASE_ASSETS_URL =
process.env.NEXT_PUBLIC_ENVIRONMENT === 'staging'
? 'https://frontend-assets.supabase.green'
: 'https://frontend-assets.supabase.com'
const POSTHOG_URL = isDevOrStaging ? 'https://ph.supabase.green' : 'https://ph.supabase.com'
const USERCENTRICS_URLS = 'https://*.usercentrics.eu'
const USERCENTRICS_APP_URL = 'https://app.usercentrics.eu'
// used by vercel live preview
const PUSHER_URL = 'https://*.pusher.com'
const PUSHER_URL_WS = 'wss://*.pusher.com'
const GOOGLE_MAPS_API_URL = 'https://maps.googleapis.com'
export function getCSP() {
const DEFAULT_SRC_URLS = [
API_URL,
SUPABASE_URL,
GOTRUE_URL,
MARKETPLACE_API_URL,
SUPABASE_LOCAL_PROJECTS_URL_WS,
SUPABASE_PROJECTS_URL,
SUPABASE_PROJECTS_URL_WS,
HCAPTCHA_SUBDOMAINS_URL,
CONFIGCAT_URL,
CONFIGCAT_PROXY_URL,
STRIPE_SUBDOMAINS_URL,
STRIPE_NETWORK_URL,
CLOUDFLARE_URL,
VERCEL_INSIGHTS_URL,
GITHUB_API_URL,
GITHUB_USER_CONTENT_URL,
SUPABASE_ASSETS_URL,
USERCENTRICS_URLS,
STAPE_URL,
GOOGLE_MAPS_API_URL,
POSTHOG_URL,
...(!!NIMBUS_PROD_PROJECTS_URL ? [NIMBUS_PROD_PROJECTS_URL, NIMBUS_PROD_PROJECTS_URL_WS] : []),
CLOUDFLARE_CDN_URL,
]
const SCRIPT_SRC_URLS = [
CLOUDFLARE_CDN_URL,
HCAPTCHA_JS_URL,
STRIPE_JS_URL,
SUPABASE_ASSETS_URL,
STAPE_URL,
POSTHOG_URL,
USERCENTRICS_URLS,
]
const FRAME_SRC_URLS = [
HCAPTCHA_ASSET_URL,
STRIPE_JS_URL,
STAPE_URL,
...(isDevOrStaging ? [POSTHOG_URL] : []),
]
const IMG_SRC_URLS = [
SUPABASE_URL,
SUPABASE_COM_URL,
SUPABASE_PROJECTS_URL,
GITHUB_USER_AVATAR_URL,
GOOGLE_USER_AVATAR_URL,
SUPABASE_ASSETS_URL,
USERCENTRICS_APP_URL,
STAPE_URL,
USERCENTRICS_URLS,
MARKETPLACE_API_URL,
...(!!NIMBUS_PROD_PROJECTS_URL ? [NIMBUS_PROD_PROJECTS_URL, NIMBUS_PROD_PROJECTS_URL_WS] : []),
]
const STYLE_SRC_URLS = [CLOUDFLARE_CDN_URL, SUPABASE_ASSETS_URL]
const FONT_SRC_URLS = [CLOUDFLARE_CDN_URL, SUPABASE_ASSETS_URL]
const defaultSrcDirective = [
`default-src 'self'`,
...DEFAULT_SRC_URLS,
...(isDevOrStaging
? [
SUPABASE_STAGING_PROJECTS_URL,
SUPABASE_STAGING_PROJECTS_URL_WS,
NIMBUS_STAGING_PROJECTS_URL,
NIMBUS_STAGING_PROJECTS_URL_WS,
VERCEL_LIVE_URL,
SUPABASE_DOCS_PROJECT_URL,
SUPABASE_CONTENT_API_URL,
]
: []),
PUSHER_URL_WS,
SENTRY_URL,
].join(' ')
const imgSrcDirective = [
`img-src 'self'`,
`blob:`,
`data:`,
...IMG_SRC_URLS,
...(isDevOrStaging
? [SUPABASE_STAGING_PROJECTS_URL, NIMBUS_STAGING_PROJECTS_URL, VERCEL_URL]
: []),
].join(' ')
const scriptSrcDirective = [
`script-src 'self'`,
`'unsafe-eval'`,
`'unsafe-inline'`,
...SCRIPT_SRC_URLS,
VERCEL_LIVE_URL,
PUSHER_URL,
GOOGLE_MAPS_API_URL,
].join(' ')
const frameSrcDirective = [`frame-src 'self'`, ...FRAME_SRC_URLS, VERCEL_LIVE_URL].join(' ')
const styleSrcDirective = [
`style-src 'self'`,
`'unsafe-inline'`,
...STYLE_SRC_URLS,
VERCEL_LIVE_URL,
].join(' ')
const fontSrcDirective = [`font-src 'self'`, ...FONT_SRC_URLS, VERCEL_LIVE_URL].join(' ')
const workerSrcDirective = [`worker-src 'self'`, `blob:`, `data:`].join(' ')
const connectSrcDirective = [
`connect-src 'self'`,
`data:`,
`blob:`,
...DEFAULT_SRC_URLS,
...(isDevOrStaging
? [
SUPABASE_STAGING_PROJECTS_URL,
SUPABASE_STAGING_PROJECTS_URL_WS,
NIMBUS_STAGING_PROJECTS_URL,
NIMBUS_STAGING_PROJECTS_URL_WS,
VERCEL_LIVE_URL,
SUPABASE_DOCS_PROJECT_URL,
SUPABASE_CONTENT_API_URL,
]
: []),
PUSHER_URL_WS,
SENTRY_URL,
].join(' ')
const cspDirectives = [
connectSrcDirective,
defaultSrcDirective,
imgSrcDirective,
scriptSrcDirective,
frameSrcDirective,
styleSrcDirective,
fontSrcDirective,
workerSrcDirective,
`object-src 'none'`,
`base-uri 'self'`,
`form-action 'self'`,
`frame-ancestors 'none'`,
`block-all-mixed-content`,
...(process.env.NEXT_PUBLIC_IS_PLATFORM === 'true' &&
process.env.NEXT_PUBLIC_ENVIRONMENT === 'prod'
? [`upgrade-insecure-requests`]
: []),
]
const csp = cspDirectives.join('; ') + ';'
// Replace newline characters and spaces
return csp.replace(/\s{2,}/g, ' ').trim()
}