fix(datatables): the database name is taken exactly, the export is admin-only, and a dropped clone forgets its row

This commit is contained in:
Diego Imbert
2026-09-07 11:37:04 +02:00
parent 2457db4da4
commit 47adfc0844
3 changed files with 39 additions and 11 deletions
@@ -1354,18 +1354,37 @@ pub struct DropForkedDatatableDatabasesRequest {
/// was created in. The permissions row stays until the drop succeeds — with its
/// logins gone every role is refused meanwhile — and is forgotten by the caller
/// once the database is. Returns the key of that database's permissions when
/// roles were dropped.
/// this workspace owns them and nothing stands in the way of forgetting them:
/// a row with no logins to drop included, or the database's next namesake would
/// find it governed by a workspace that is gone.
async fn drop_datatable_roles_before_its_database(
db: &DB,
w_id: &str,
dt_name: &str,
) -> Option<String> {
let planned =
crate::datatable_permissions::plan_drop_of_datatable_roles(db, w_id, dt_name).await?;
let key = planned.2.clone();
crate::datatable_permissions::run_planned_drop_keeping_record(db, w_id, dt_name, planned)
let (_, _, key) =
windmill_common::workspaces::resolve_datatable_database_unchecked(db, w_id, dt_name)
.await
.ok()?;
let record = windmill_common::workspaces::database_permissions_by_key(db, &key)
.await
.then_some(key)
.ok()
.flatten()?;
if record.owner_workspace_id.as_deref() != Some(w_id) {
return None;
}
if let Some(planned) =
crate::datatable_permissions::plan_drop_of_datatable_roles(db, w_id, dt_name).await
{
if !crate::datatable_permissions::run_planned_drop_keeping_record(
db, w_id, dt_name, planned,
)
.await
{
return None;
}
}
Some(key)
}
/// Drop forked datatable databases. Returns errors per datatable that failed.
@@ -1764,8 +1764,12 @@ pub(crate) async fn tarball_workspace(
// the database, with roles and tenants — no passwords, which are direct
// database logins, and no login names, which a re-save generates. A database
// no entry of the workspace reaches any more cannot be named, and is left out.
let permissions =
windmill_common::workspaces::database_permissions_owned_by(&mut *tx, &w_id).await?;
// Admin-only, like the settings and like the drawer that shows the same thing.
let permissions = if include_settings.unwrap_or(false) {
windmill_common::workspaces::database_permissions_owned_by(&mut *tx, &w_id).await?
} else {
vec![]
};
if !permissions.is_empty() {
let mut reaching: std::collections::HashMap<String, String> =
std::collections::HashMap::new();
+8 -3
View File
@@ -1300,12 +1300,13 @@ pub fn datatable_database_key(
use sha2::{Digest, Sha256};
// As the connection reads them, not as the JSON spells them: a port
// left out is 5432, a number and its string are one port, a host is
// one host whatever its case.
// one host whatever its case. A database name is taken exactly — `"prod "`
// is a database of its own to Postgres.
let text = |field: &str| {
resolved
.get(field)
.map(|v| match v.as_str() {
Some(s) => s.trim().to_string(),
Some(s) => s.to_string(),
None => v.to_string(),
})
.unwrap_or_default()
@@ -3476,7 +3477,11 @@ mod tests {
// A host is one host whatever its case, as DNS reads it.
assert_eq!(
datatable_database_key(&pg("u/a/pg"), &resolved("db.example", "prod", "app")),
datatable_database_key(&pg("u/a/pg"), &resolved(" DB.Example ", "prod", "app"))
datatable_database_key(&pg("u/a/pg"), &resolved("DB.Example", "prod", "app"))
);
assert_ne!(
datatable_database_key(&pg("u/a/pg"), &resolved("db", "prod", "app")),
datatable_database_key(&pg("u/a/pg"), &resolved("db", "prod ", "app"))
);
}