From eb95a95f486aeb55a63dff53038c2c598eabb465 Mon Sep 17 00:00:00 2001 From: Diego Imbert Date: Mon, 31 Aug 2026 06:52:03 +0200 Subject: [PATCH] feat(datatables): a permissions drawer on a table --- .../src/datatable_acl.rs | 30 ++++++++++-- frontend/src/lib/components/DBManager.svelte | 47 ++++++++++++------- .../datatableAcl/PgAclEditor.svelte | 6 +-- 3 files changed, 61 insertions(+), 22 deletions(-) diff --git a/backend/windmill-api-workspaces/src/datatable_acl.rs b/backend/windmill-api-workspaces/src/datatable_acl.rs index ed5cfebffe..814229abaa 100644 --- a/backend/windmill-api-workspaces/src/datatable_acl.rs +++ b/backend/windmill-api-workspaces/src/datatable_acl.rs @@ -363,8 +363,12 @@ fn plan_statements( } // Ownership cannot be set ahead of time: an object belongs to // whoever creates it. Default privileges are what keeps the owner - // in reach of what the other roles create from here on. - for other in other_pg_roles { + // in reach of what the other roles create from here on — which only + // means something for a schema, the thing objects are created in. + for other in other_pg_roles + .iter() + .filter(|_| matches!(target, AclTarget::Schema { .. })) + { for plural in ["TABLES", "SEQUENCES", "FUNCTIONS"] { statements.push(format!( "ALTER DEFAULT PRIVILEGES FOR ROLE {} IN SCHEMA {} GRANT ALL PRIVILEGES ON {} TO {}", @@ -375,7 +379,7 @@ fn plan_statements( )); } } - if existing_objects.is_empty() { + if existing_objects.is_empty() && matches!(target, AclTarget::Schema { .. }) { warnings.push(format!( "{} holds no objects yet; only the schema itself changes hands.", target.label(dbname) @@ -1053,6 +1057,26 @@ mod tests { } } + #[test] + fn a_tables_owner_change_is_only_that_table() { + let plan = plan_statements( + &AclTarget::Table { schema: "analytics".to_string(), table: "orders".to_string() }, + &AclChange::SetOwner { role: "analyst".to_string() }, + "dt_probe", + "wm_analyst_1", + &["wm_admin".to_string()], + &[], + ) + .unwrap(); + // Default privileges are about what gets created in a schema, which + // changing one table's owner says nothing about. + assert_eq!( + plan.statements, + [r#"ALTER TABLE "analytics"."orders" OWNER TO "wm_analyst_1""#] + ); + assert!(plan.warnings.is_empty()); + } + #[test] fn several_objects_are_revoked_together() { let plan = plan_statements( diff --git a/frontend/src/lib/components/DBManager.svelte b/frontend/src/lib/components/DBManager.svelte index b318ab8b1c..1c39060b12 100644 --- a/frontend/src/lib/components/DBManager.svelte +++ b/frontend/src/lib/components/DBManager.svelte @@ -38,7 +38,7 @@ import PgAclEditor from './datatableAcl/PgAclEditor.svelte' import { favoriteManager } from './sidebar/FavoriteMenu.svelte' import DatatableRoleBadge from './DatatableRoleBadge.svelte' - import type { Asset, DataTableTables } from '$lib/gen' + import type { AclTarget, Asset, DataTableTables } from '$lib/gen' import { ADMIN_DATATABLE_ROLE, type DatatableRowAction } from './dbTypes' import TextInput from './text_input/TextInput.svelte' import Checkbox from './common/checkbox/Checkbox.svelte' @@ -287,8 +287,8 @@ // overrides is what lets the current data table and selected schema — which // default to open — actually be folded; a plain "expanded" set could never // close them, since the default would keep winning. - // The schema the permissions drawer is open on, if any. - let schemaPermissions = $state<{ datatable: string | undefined; schema: string } | undefined>( + // What the permissions drawer is open on, if anything: a schema, or a table. + let aclDrawer = $state<{ datatable: string | undefined; target: AclTarget } | undefined>( undefined ) @@ -536,6 +536,12 @@ ) let newSchemaName = $state('') + /** What the drawer is about, for its title. */ + function aclLabel(target: AclTarget | undefined): string { + if (!target) return '' + return target.kind === 'table' ? `${target.schema}.${target.table}` : (target.schema ?? '') + } + function closeSchemaDialog() { schemaDialog = undefined newSchemaName = '' @@ -727,9 +733,9 @@ displayName: 'Permissions', icon: KeyRoundIcon, action: () => - (schemaPermissions = { + (aclDrawer = { datatable: root.datatable, - schema: sc.schemaKey + target: { kind: 'schema', schema: sc.schemaKey } }) }, { @@ -805,6 +811,19 @@ [ + { + displayName: 'Permissions', + icon: KeyRoundIcon, + action: () => + (aclDrawer = { + datatable: root.datatable, + target: { + kind: 'table', + schema: sc.schemaKey, + table: tableKey + } + }) + }, { displayName: 'Delete table', icon: Trash2Icon, @@ -886,20 +905,16 @@ - (schemaPermissions = undefined)}> + (aclDrawer = undefined)}> (schemaPermissions = undefined)} - tooltip="Who owns this schema, and what each role may do in it. Runs against the data table as its admin connection." + title="Permissions — {aclLabel(aclDrawer?.target)}" + on:close={() => (aclDrawer = undefined)} + tooltip="Who owns this, and what each role may do with it. Runs against the data table as its admin connection." > - {#if schemaPermissions && workspace} - {@const dt = schemaPermissions.datatable ?? currentDatatable} + {#if aclDrawer && workspace} + {@const dt = aclDrawer.datatable ?? currentDatatable} {#if dt} - + {/if} {/if} diff --git a/frontend/src/lib/components/datatableAcl/PgAclEditor.svelte b/frontend/src/lib/components/datatableAcl/PgAclEditor.svelte index b21b65fe74..15582eb9dc 100644 --- a/frontend/src/lib/components/datatableAcl/PgAclEditor.svelte +++ b/frontend/src/lib/components/datatableAcl/PgAclEditor.svelte @@ -106,9 +106,9 @@
Owner - The role that owns {target.kind === 'schema' ? 'the schema' : 'the table'} and everything - already in it. Changing it also lets the new owner reach what the other roles create here - later. + {target.kind === 'schema' + ? 'The role that owns the schema and everything already in it. Changing it also lets the new owner reach what the other roles create here later.' + : 'The role that owns the table. Its owner may always read and write it, and is who ALTER and DROP answer to.'}