fix(datatables): close the fail-open in role resolution, and gate the ACL endpoints

A role with a stored pg_rolename but no password resolved to the data table's
own connection, which owns everything — so a caller authorized as one role got
the admin one instead. Exports and git-synced settings redact that password, so
a restored config is exactly the shape that produced it. Refuse instead, and
name the fix.

The ACL endpoints took any workspace member: on a data table without roles
every member resolves to that same admin connection, so ownership and grants
there are the workspace admins' to change, as the roles themselves are.

Also: check a migration batch's roles before applying any of it, keep the role
picker for a single non-default role, drop the revoke button from a default
privilege on types (which no scope can express), and say what
get_datatable_resource_as_default_role actually resolves as.
This commit is contained in:
Diego Imbert
2026-09-01 04:48:44 +02:00
parent 28b31a437e
commit 45d5eeda0e
12 changed files with 105 additions and 45 deletions
@@ -2472,10 +2472,11 @@ async fn get_datatable_table_schema(
}))
}
/// Resolve a data table for an API caller. Schema browsing and the database
/// manager always connect as `root`, so a permissioned data table is reachable
/// from the UI only by a tenant of its `root` role (and by admins).
async fn get_datatable_resource_as_admin(
/// Resolve a data table for an API caller as the role it defaults to — schema
/// browsing, the database manager and copy-between-databases name none — so a
/// permissioned data table is reachable from the UI only by a tenant of that
/// role (and by workspace admins, who may use any of them).
async fn get_datatable_resource_as_default_role(
db: &DB,
authed: &ApiAuthed,
w_id: &str,
@@ -2514,7 +2515,7 @@ async fn get_datatable_schema(
datatable_name: &str,
) -> Result<SchemaMap> {
// Get the datatable resource (connection credentials)
let db_resource = get_datatable_resource_as_admin(db, authed, w_id, datatable_name).await?;
let db_resource = get_datatable_resource_as_default_role(db, authed, w_id, datatable_name).await?;
// Parse the resource as PgDatabase
let pg_db: PgDatabase = serde_json::from_value(db_resource)
@@ -2763,7 +2764,7 @@ async fn get_datatable_table_columns(
)));
}
let db_resource = get_datatable_resource_as_admin(db, authed, w_id, datatable_name).await?;
let db_resource = get_datatable_resource_as_default_role(db, authed, w_id, datatable_name).await?;
let pg_db: PgDatabase = serde_json::from_value(db_resource)
.map_err(|e| Error::internal_err(format!("Failed to parse database credentials: {}", e)))?;
let (client, connection) = pg_db.connect(Some(db)).await?;
@@ -2986,7 +2987,7 @@ pub(crate) async fn resolve_pg_source_checked(
source: &str,
) -> Result<PgDatabase> {
let db_resource = if let Some(name) = source.strip_prefix("datatable://") {
get_datatable_resource_as_admin(db, authed, w_id, name).await?
get_datatable_resource_as_default_role(db, authed, w_id, name).await?
} else if let Some(path) = source.strip_prefix("$res:") {
let db_with_authed = windmill_common::db::DbWithOptAuthed::from_authed(
authed,