fix(datatables): give custom_instance_user the grant option on instance databases

This commit is contained in:
Diego Imbert
2026-08-31 05:07:01 +02:00
parent ce89023cef
commit 395eac68c7
2 changed files with 71 additions and 9 deletions
@@ -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,
+48 -8
View File
@@ -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!(