diff --git a/backend/windmill-api-workspaces/src/workspaces.rs b/backend/windmill-api-workspaces/src/workspaces.rs index 52de762c57..2071028b95 100644 --- a/backend/windmill-api-workspaces/src/workspaces.rs +++ b/backend/windmill-api-workspaces/src/workspaces.rs @@ -2104,6 +2104,13 @@ struct DataTableTables { datatable_name: String, /// Hierarchical metadata: schema_name -> table_names schemas: TableListMap, + /// The schemas the connection's role may create tables in — a subset of + /// `schemas`, since reaching one says nothing about writing to it. + #[serde(default)] + creatable_schemas: Vec, + /// Whether that role may create schemas in the database at all. + #[serde(default)] + can_create_schema: bool, #[serde(skip_serializing_if = "Option::is_none")] error: Option, /// The roles the caller may run this data table as. Empty when it has no @@ -2289,12 +2296,20 @@ async fn list_datatable_tables( .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 { - Ok(schemas) => { - DataTableTables { datatable_name, schemas, error: None, usable_roles, default_role } - } + Ok(contents) => DataTableTables { + datatable_name, + schemas: contents.tables, + creatable_schemas: contents.creatable_schemas, + can_create_schema: contents.can_create_schema, + error: None, + usable_roles, + default_role, + }, Err(e) => DataTableTables { datatable_name, schemas: HashMap::new(), + creatable_schemas: vec![], + can_create_schema: false, error: Some(e.to_string()), usable_roles, default_role, @@ -2505,12 +2520,19 @@ async fn get_datatable_schema( Ok(schema_map) } +/// What exists in a data table, and what its role may add to it. +struct DataTableContents { + tables: TableListMap, + creatable_schemas: Vec, + can_create_schema: bool, +} + async fn get_datatable_tables( db: &DB, authed: &ApiAuthed, w_id: &str, datatable_name: &str, -) -> Result { +) -> Result { let db_resource = get_datatable_resource_as_admin(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)))?; @@ -2525,7 +2547,8 @@ async fn get_datatable_tables( let schema_rows = client .query( r#" - SELECT nspname::text AS schema_name + SELECT nspname::text AS schema_name, + has_schema_privilege(oid, 'CREATE') AS can_create FROM pg_namespace WHERE nspname NOT IN ('information_schema', 'pg_toast', 'pg_catalog') AND nspname NOT LIKE 'pg_%' @@ -2542,15 +2565,33 @@ async fn get_datatable_tables( })?; let mut table_map: TableListMap = HashMap::new(); + let mut creatable_schemas: Vec = Vec::new(); let schema_names: Vec = schema_rows .iter() .map(|row| { let name: String = row.get(0); table_map.entry(name.clone()).or_default(); + if row.get::<_, bool>(1) { + creatable_schemas.push(name.clone()); + } name }) .collect(); + let can_create_schema: bool = client + .query_one( + "SELECT has_database_privilege(current_database(), 'CREATE')", + &[], + ) + .await + .map_err(|e| { + Error::internal_err(format!( + "Failed to read the database privileges: {}", + pg_error_message(&e) + )) + })? + .get(0); + let rows = client .query( r#" @@ -2575,7 +2616,7 @@ async fn get_datatable_tables( table_map.entry(table_schema).or_default().push(table_name); } - Ok(table_map) + Ok(DataTableContents { tables: table_map, creatable_schemas, can_create_schema }) } async fn get_datatable_table_columns( diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 8435af89bb..2a273e2dde 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -34063,6 +34063,14 @@ components: type: string error: type: string + creatable_schemas: + type: array + description: the schemas the caller's role may create tables in + items: + type: string + can_create_schema: + type: boolean + description: whether that role may create schemas in the database usable_roles: type: array description: roles the caller may run this data table as, empty when it has no permissions diff --git a/frontend/src/lib/components/DBManager.svelte b/frontend/src/lib/components/DBManager.svelte index da80d143d6..7a46b851dc 100644 --- a/frontend/src/lib/components/DBManager.svelte +++ b/frontend/src/lib/components/DBManager.svelte @@ -220,6 +220,23 @@ return datatableTree?.find((d) => d.datatable_name === datatable)?.error } + /** Whether the role this connection uses may create a table in that schema, + * or a schema at all. A plain database reports nothing, and there the buttons + * stay: hiding them on no information would be worse than a refusal. */ + function canCreateTableIn(datatable: string | undefined, schemaKey: string): boolean { + const entry = datatableTree?.find((d) => d.datatable_name === datatable) + if (!entry?.creatable_schemas) return true + // A schema created since the snapshot is not in it yet; the role just made + // it, so it can write to it. + if (!(schemaKey in entry.schemas)) return true + return entry.creatable_schemas.includes(schemaKey) + } + + function canCreateSchemaIn(datatable: string | undefined): boolean { + const entry = datatableTree?.find((d) => d.datatable_name === datatable) + return entry === undefined || !!entry.can_create_schema + } + /** The role a data table row is reached through, and what it can be switched * to. Absent where naming the role says nothing: a data table without * permissions, or one whose single usable role is already the implicit @@ -737,17 +754,19 @@ {/each} - + {#if canCreateTableIn(root.datatable, sc.schemaKey)} + + {/if} {/if} {/each} - {#if dbSupportsSchemas && search.trim() === ''} + {#if dbSupportsSchemas && search.trim() === '' && canCreateSchemaIn(root.datatable)}