From 57ad009f9f12c876ee09ba2b2a6e7a71dcdb1ca7 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Sun, 14 Jun 2026 22:24:19 +0000 Subject: [PATCH] fix: create windmill_admin without BYPASSRLS on managed dbs On managed Postgres (Scaleway, RDS, Azure, ...) the connecting user is not a superuser and cannot create a role WITH BYPASSRLS. The early migrations create windmill_admin WITH BYPASSRLS inside DO/EXCEPTION blocks that swallow the resulting "permission denied to create role" error, so the role is never created. The first bare `GRANT ... TO windmill_admin` migration (20221211192539) then hard-fails with `role "windmill_admin" does not exist`, aborting all initial migrations. Bootstrap windmill_user and windmill_admin before migrations run, falling back to a plain windmill_admin (no BYPASSRLS) when BYPASSRLS is not permitted. Admin access remains correct in that case via the explicit admin_policy (USING true) entries already created for every RLS table. Fixes WIN-2042 Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/windmill-api/src/db.rs | 45 ++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/backend/windmill-api/src/db.rs b/backend/windmill-api/src/db.rs index c8ed841e19..9622d6007b 100644 --- a/backend/windmill-api/src/db.rs +++ b/backend/windmill-api/src/db.rs @@ -254,10 +254,55 @@ impl Migrate for CustomMigrator { } } +/// Ensure the `windmill_user` and `windmill_admin` roles exist before migrations run. +/// +/// The early migrations create these roles inside `DO`/`EXCEPTION` blocks that swallow any +/// error, and `windmill_admin` is created `WITH BYPASSRLS`. On managed Postgres (Scaleway, +/// RDS, Azure, ...) the connecting user is not a superuser and cannot create a BYPASSRLS +/// role, so that creation silently fails and the role never exists — which then makes the +/// first bare `GRANT ... TO windmill_admin` migration hard-fail. We bootstrap the roles here, +/// falling back to a plain `windmill_admin` (no BYPASSRLS) when BYPASSRLS is not permitted; +/// admin access is then enforced by the per-table `FOR ALL TO windmill_admin USING (true)` +/// policies that the migrations create on every RLS-enabled table. +async fn ensure_windmill_roles(db: &DB) { + let bootstrap = r#" +DO +$$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'windmill_user') THEN + BEGIN + CREATE ROLE windmill_user; + EXCEPTION WHEN OTHERS THEN + RAISE NOTICE 'Could not create windmill_user role: %', SQLERRM; + END; + END IF; + + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'windmill_admin') THEN + BEGIN + CREATE ROLE windmill_admin WITH BYPASSRLS; + EXCEPTION WHEN OTHERS THEN + BEGIN + CREATE ROLE windmill_admin; + RAISE NOTICE 'Created windmill_admin without BYPASSRLS (managed db without superuser)'; + EXCEPTION WHEN OTHERS THEN + RAISE NOTICE 'Could not create windmill_admin role: %', SQLERRM; + END; + END; + END IF; +END +$$; +"#; + if let Err(err) = sqlx::query(bootstrap).execute(db).await { + tracing::info!("Could not bootstrap windmill roles: {err:#}"); + } +} + pub async fn migrate( db: &DB, mut killpill_rx: tokio::sync::broadcast::Receiver<()>, ) -> Result>, Error> { + ensure_windmill_roles(db).await; + let migrator = db.acquire().await?; let mut custom_migrator = CustomMigrator { inner: migrator };