mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-06 00:02:13 +00:00
feat(datatables): only offer create where the role may create
This commit is contained in:
@@ -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<String>,
|
||||
/// 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<String>,
|
||||
/// 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<String>,
|
||||
can_create_schema: bool,
|
||||
}
|
||||
|
||||
async fn get_datatable_tables(
|
||||
db: &DB,
|
||||
authed: &ApiAuthed,
|
||||
w_id: &str,
|
||||
datatable_name: &str,
|
||||
) -> Result<TableListMap> {
|
||||
) -> Result<DataTableContents> {
|
||||
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<String> = Vec::new();
|
||||
let schema_names: Vec<String> = 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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 @@
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
<button
|
||||
class={'w-full text-xs font-normal flex gap-2 items-center h-8 cursor-pointer pr-1 hover:bg-gray-500/10 text-secondary ' +
|
||||
tableIndent}
|
||||
onclick={() => startCreateTable(root.datatable, sc.schemaKey)}
|
||||
>
|
||||
<Plus class="shrink-0" size={14} />
|
||||
<span class="text-xs">New table</span>
|
||||
</button>
|
||||
{#if canCreateTableIn(root.datatable, sc.schemaKey)}
|
||||
<button
|
||||
class={'w-full text-xs font-normal flex gap-2 items-center h-8 cursor-pointer pr-1 hover:bg-gray-500/10 text-secondary ' +
|
||||
tableIndent}
|
||||
onclick={() => startCreateTable(root.datatable, sc.schemaKey)}
|
||||
>
|
||||
<Plus class="shrink-0" size={14} />
|
||||
<span class="text-xs">New table</span>
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
{#if dbSupportsSchemas && search.trim() === ''}
|
||||
{#if dbSupportsSchemas && search.trim() === '' && canCreateSchemaIn(root.datatable)}
|
||||
<button
|
||||
class={'w-full text-xs font-normal flex gap-2 items-center h-8 cursor-pointer pr-1 hover:bg-gray-500/10 text-secondary ' +
|
||||
(root.datatable !== undefined ? 'pl-7' : 'pl-3')}
|
||||
|
||||
Reference in New Issue
Block a user