From 395eac68c74a9a79988cf322bdf1a82bf6e9023b Mon Sep 17 00:00:00 2001 From: Diego Imbert Date: Mon, 31 Aug 2026 05:07:01 +0200 Subject: [PATCH] fix(datatables): give custom_instance_user the grant option on instance databases --- .../src/datatable_migrations.rs | 24 +++++++- backend/windmill-common/src/lib.rs | 56 ++++++++++++++++--- 2 files changed, 71 insertions(+), 9 deletions(-) diff --git a/backend/windmill-api-workspaces/src/datatable_migrations.rs b/backend/windmill-api-workspaces/src/datatable_migrations.rs index eaa37b73df..ae3ca7a527 100644 --- a/backend/windmill-api-workspaces/src/datatable_migrations.rs +++ b/backend/windmill-api-workspaces/src/datatable_migrations.rs @@ -35,7 +35,10 @@ use windmill_common::runnable_settings::{ConcurrencySettingsWithCustom, Debounci use windmill_common::scripts::ScriptLang; use windmill_common::users::username_to_permissioned_as; use windmill_common::worker::{to_raw_value, SqlAnnotations}; -use windmill_common::workspaces::get_datatable_resource_from_db_unchecked; +use windmill_common::ensure_instance_db_grant_options; +use windmill_common::workspaces::{ + get_datatable_resource_from_db_unchecked, DataTableCatalogResourceType, +}; use windmill_common::{PgDatabase, DB}; use windmill_git_sync::{ handle_deployment_metadata, handle_deployment_metadata_batch, DeployedObject, @@ -854,6 +857,25 @@ async fn enable_datatable_migrations( ))); } + // A data table provisioned before instance grants carried `WITH GRANT + // OPTION` cannot hand its privileges to the roles the permissions feature + // creates. Opting in to migrations is where an admin passes through, so + // repair it here; it is a no-op for one that already has them, and only + // applies to instance databases, where Windmill owns the Postgres user. + if let Ok(datatable) = crate::datatable_permissions::read_datatable(&db, &w_id, &datatable_name).await { + if datatable.database.resource_type == DataTableCatalogResourceType::Instance { + if let Err(e) = + ensure_instance_db_grant_options(&db, &datatable.database.resource_path).await + { + tracing::warn!( + "Could not refresh the grant options of instance database '{}': {}. Continuing.", + datatable.database.resource_path, + e + ); + } + } + } + audit_log( &db, &authed, diff --git a/backend/windmill-common/src/lib.rs b/backend/windmill-common/src/lib.rs index 89db0ee769..ea4b8e5045 100644 --- a/backend/windmill-common/src/lib.rs +++ b/backend/windmill-common/src/lib.rs @@ -1397,6 +1397,53 @@ pub async fn drop_custom_instance_database(db: &DB, dbname: &str) -> error::Resu Ok(()) } +/// What `custom_instance_user` gets on an instance database. +/// +/// `WITH GRANT OPTION` throughout: data table permissions have Windmill connect +/// as this role and hand privileges to the Postgres roles it creates, and a +/// privilege it cannot pass on is one those roles can never receive. The +/// database and schema `public` are owned by the instance's own Postgres user, +/// not by this one, so re-granting is the only way it can reach them. +fn instance_db_grants(dbname: &str) -> String { + format!( + "GRANT CONNECT ON DATABASE \"{dbname}\" TO custom_instance_user WITH GRANT OPTION; + GRANT USAGE ON SCHEMA public TO custom_instance_user WITH GRANT OPTION; + GRANT CREATE ON SCHEMA public TO custom_instance_user WITH GRANT OPTION; + GRANT CREATE ON DATABASE \"{dbname}\" TO custom_instance_user WITH GRANT OPTION; + ALTER DEFAULT PRIVILEGES IN SCHEMA public + GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO custom_instance_user;" + ) +} + +/// Re-apply those grants to an instance database that already exists. +/// +/// One provisioned before the grant options were added holds the privileges +/// without them, and only the instance's own Postgres user — the connection +/// this runs on — can add them: a role cannot give itself a grant option it +/// does not hold. +pub async fn ensure_instance_db_grant_options(db: &DB, dbname: &str) -> error::Result<()> { + let dbname = dbname.trim(); + validate_dbname(dbname)?; + + let wmill_pg_creds = PgDatabase::parse_uri(&get_database_url().await?.as_str().await)?; + let creds = PgDatabase { dbname: dbname.to_string(), ..wmill_pg_creds }; + let (client, connection) = creds.connect(Some(db)).await?; + let join_handle = tokio::spawn(async move { connection.await }); + + let result = client.batch_execute(&instance_db_grants(dbname)).await; + + drop(client); + shutdown_pg_connection(join_handle).await?; + + result.map_err(|e| { + error::Error::internal_err(format!( + "Failed to grant permissions on '{}': {}", + dbname, + crate::error::pg_error_message(&e) + )) + }) +} + /// Create a custom instance database: CREATE DATABASE, grant permissions, register in global_settings. /// The `tag` is stored in global_settings metadata (e.g. "datatable" or "ducklake"). pub async fn create_custom_instance_database( @@ -1437,14 +1484,7 @@ pub async fn create_custom_instance_database( let join_handle = tokio::spawn(async move { connection.await }); if let Err(e) = client - .batch_execute(&format!( - "GRANT CONNECT ON DATABASE \"{dbname}\" TO custom_instance_user; - GRANT USAGE ON SCHEMA public TO custom_instance_user; - GRANT CREATE ON SCHEMA public TO custom_instance_user; - GRANT CREATE ON DATABASE \"{dbname}\" TO custom_instance_user; - ALTER DEFAULT PRIVILEGES IN SCHEMA public - GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO custom_instance_user;" - )) + .batch_execute(&instance_db_grants(dbname)) .await { tracing::warn!(