From e989a4d9ae534792579ce04a7e1736f58a689650 Mon Sep 17 00:00:00 2001 From: Diego Imbert Date: Mon, 31 Aug 2026 06:01:13 +0200 Subject: [PATCH] fix(datatables): report create privileges for the role the manager is on --- .../windmill-api-workspaces/src/workspaces.rs | 29 ++++++++++++++-- backend/windmill-api/openapi.yaml | 12 +++++++ .../src/lib/components/DBManagerDrawer.svelte | 33 ++++++++++++++----- 3 files changed, 63 insertions(+), 11 deletions(-) diff --git a/backend/windmill-api-workspaces/src/workspaces.rs b/backend/windmill-api-workspaces/src/workspaces.rs index 2071028b95..3d23b747d0 100644 --- a/backend/windmill-api-workspaces/src/workspaces.rs +++ b/backend/windmill-api-workspaces/src/workspaces.rs @@ -2282,10 +2282,20 @@ async fn list_datatable_schemas( Ok(Json(results)) } +/// The role the caller is on, and the data table it belongs to. The tree covers +/// every data table, and a role name means nothing outside the one it was +/// picked on, so it is named rather than applied to all of them. +#[derive(Deserialize)] +struct ListDataTableTablesQuery { + role_for: Option, + role: Option, +} + async fn list_datatable_tables( authed: ApiAuthed, Extension(db): Extension, Path(w_id): Path, + Query(query): Query, ) -> JsonResult> { let datatable_names = list_datatable_names(&db, &w_id).await?; let mut roles = list_datatable_roles(&db, &authed, &w_id).await?; @@ -2295,7 +2305,14 @@ async fn list_datatable_tables( let (usable_roles, default_role) = roles .remove(&datatable_name) .unwrap_or_else(|| (vec![], ADMIN_DATATABLE_ROLE.to_string())); - let tables = match get_datatable_tables(&db, &authed, &w_id, &datatable_name).await { + // Privileges are the connected role's, so the answer for the data table + // the caller is on has to be read as the role they picked, not as the + // one it defaults to. + let role = query + .role + .as_deref() + .filter(|_| query.role_for.as_deref() == Some(datatable_name.as_str())); + let tables = match get_datatable_tables(&db, &authed, &w_id, &datatable_name, role).await { Ok(contents) => DataTableTables { datatable_name, schemas: contents.tables, @@ -2532,8 +2549,16 @@ async fn get_datatable_tables( authed: &ApiAuthed, w_id: &str, datatable_name: &str, + role: Option<&str>, ) -> Result { - let db_resource = get_datatable_resource_as_admin(db, authed, w_id, datatable_name).await?; + let db_resource = get_datatable_resource_from_db( + db, + w_id, + datatable_name, + role, + DatatableAccess::Authed(authed.to_authed_ref()), + ) + .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?; diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 2a273e2dde..c447fe9d40 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -4865,6 +4865,18 @@ paths: - workspace parameters: - $ref: "#/components/parameters/WorkspaceId" + - name: role_for + in: query + required: false + schema: + type: string + description: the data table the `role` below was picked on + - name: role + in: query + required: false + schema: + type: string + description: read that data table's privileges as this role rather than its default responses: "200": description: table metadata of all datatables diff --git a/frontend/src/lib/components/DBManagerDrawer.svelte b/frontend/src/lib/components/DBManagerDrawer.svelte index 6e88d8e800..056db156aa 100644 --- a/frontend/src/lib/components/DBManagerDrawer.svelte +++ b/frontend/src/lib/components/DBManagerDrawer.svelte @@ -42,15 +42,21 @@ // Every data table with its schemas and tables, in one call: this is what the // left pane's tree navigates, so it has to cover the data tables the user is // not currently on, not just the selected one. - const datatables = resource([], async () => { - if (!ws) return [] - try { - return await WorkspaceService.listDataTableTables({ workspace: ws }) - } catch (e) { - console.error('Failed to load datatables:', e) - return [] - } - }) + const datatables = resource( + // The privileges it reports are the connected role's, so the role picked on + // the open data table is part of what is being asked. + () => [ws, uriState.selectedDatatable, uriState.selectedRole] as const, + async ([workspace, roleFor, role]): Promise => { + if (!workspace) return [] + try { + return await WorkspaceService.listDataTableTables({ workspace, roleFor, role }) + } catch (e) { + console.error('Failed to load datatables:', e) + return [] + } + }, + { initialValue: [] } + ) // Roles the *caller* may use, so the picker never offers one that would be // refused. Absent/disabled permissions yield no roles and hide the picker. @@ -194,6 +200,14 @@ function refreshManager() { dbManagerContent?.refresh() dbManagerContent?.dbManager()?.dbTable()?.refresh() + refreshRoles() + } + + /** Re-read what the tree and the role picker show: both are answers about the + * data table's roles, which the permissions drawer can have just changed. */ + function refreshRoles() { + datatables.refetch() + usableRoles.refetch() } async function handleExportSchema(explicitSource?: string) { @@ -327,6 +341,7 @@ hideTrigger workspace={ws} datatable={actionDatatable} + onSaved={refreshRoles} /> {/if}