mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-21 08:02:38 +00:00
feat(datatables): data table roles in the DB manager and raw apps
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a1b91690fd
commit
f36aa69fc3
@@ -13,7 +13,7 @@
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
pub(crate) use crate::datatable_permissions_ee::{
|
||||
ensure_governs_datatable, ensure_reaches_datatable, get_datatable_permissions,
|
||||
list_usable_datatable_roles, set_datatable_permissions,
|
||||
list_usable_datatable_roles, set_datatable_permissions, usable_datatable_roles,
|
||||
};
|
||||
|
||||
#[cfg(not(all(feature = "private", feature = "enterprise")))]
|
||||
@@ -70,4 +70,28 @@ mod ce {
|
||||
pub(crate) async fn list_usable_datatable_roles(_authed: ApiAuthed) -> Result<String> {
|
||||
Err(unavailable())
|
||||
}
|
||||
|
||||
pub(crate) struct UsableDatatableRoles {
|
||||
pub(crate) permissioned: bool,
|
||||
pub(crate) roles: Vec<String>,
|
||||
pub(crate) default_role: String,
|
||||
}
|
||||
|
||||
/// A data table not under roles is used as `admin`, as before roles existed. One under roles
|
||||
/// is refused: no role of it can be connected as.
|
||||
pub(crate) async fn usable_datatable_roles(
|
||||
_db: &DB,
|
||||
_authed: &ApiAuthed,
|
||||
_w_id: &str,
|
||||
governing: &GoverningDatatable,
|
||||
) -> Result<UsableDatatableRoles> {
|
||||
if governing.datatable.permissions.is_some() {
|
||||
return Err(unavailable());
|
||||
}
|
||||
Ok(UsableDatatableRoles {
|
||||
permissioned: false,
|
||||
roles: vec![],
|
||||
default_role: windmill_common::datatable_roles::ADMIN_DATATABLE_ROLE.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2269,6 +2269,25 @@ struct DataTableTables {
|
||||
schemas: TableListMap,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
error: Option<String>,
|
||||
/// On the instance database: the only kind that can be under roles or have its access edited.
|
||||
instance: bool,
|
||||
permissioned: bool,
|
||||
/// The roles this caller may connect as, by name; empty when not under roles.
|
||||
usable_roles: Vec<String>,
|
||||
default_role: String,
|
||||
/// What the role the listing connected as may create.
|
||||
can_create_schema: bool,
|
||||
creatable_schemas: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ListDataTableTablesQuery {
|
||||
/// List only this data table: each entry opens a connection to its database.
|
||||
datatable_name: Option<String>,
|
||||
/// The data table `role` applies to. Every other one is listed as its default role, since a
|
||||
/// role name means nothing outside the data table it belongs to.
|
||||
role_for: Option<String>,
|
||||
role: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -2276,6 +2295,7 @@ struct GetDataTableSchemaQuery {
|
||||
datatable_name: String,
|
||||
schema_name: String,
|
||||
table_name: String,
|
||||
role: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
@@ -2443,25 +2463,89 @@ async fn list_datatable_tables(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
Query(query): Query<ListDataTableTablesQuery>,
|
||||
) -> JsonResult<Vec<DataTableTables>> {
|
||||
let datatable_names = list_datatable_names(&db, &w_id).await?;
|
||||
if query.role.is_some() && query.role_for.is_none() {
|
||||
return Err(Error::BadRequest(
|
||||
"`role` needs `role_for`, the data table it is a role of".to_string(),
|
||||
));
|
||||
}
|
||||
if let (Some(only), Some(role_for)) =
|
||||
(query.datatable_name.as_deref(), query.role_for.as_deref())
|
||||
{
|
||||
if only != role_for {
|
||||
return Err(Error::BadRequest(format!(
|
||||
"`role_for` names '{role_for}', which `datatable_name` leaves out of the listing"
|
||||
)));
|
||||
}
|
||||
}
|
||||
let mut datatable_names = list_datatable_names(&db, &w_id).await?;
|
||||
for named in [query.role_for.as_deref(), query.datatable_name.as_deref()]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
if !datatable_names.iter().any(|n| n == named) {
|
||||
return Err(Error::NotFound(format!(
|
||||
"No data table named '{named}' in this workspace"
|
||||
)));
|
||||
}
|
||||
}
|
||||
if let Some(only) = query.datatable_name.as_deref() {
|
||||
datatable_names.retain(|n| n == only);
|
||||
}
|
||||
let mut results = Vec::new();
|
||||
|
||||
for datatable_name in datatable_names {
|
||||
let tables = match get_datatable_tables(&db, &authed, &w_id, &datatable_name).await {
|
||||
Ok(schemas) => DataTableTables { datatable_name, schemas, error: None },
|
||||
Err(e) => DataTableTables {
|
||||
datatable_name,
|
||||
schemas: HashMap::new(),
|
||||
error: Some(e.to_string()),
|
||||
},
|
||||
};
|
||||
results.push(tables);
|
||||
let role = query
|
||||
.role
|
||||
.as_deref()
|
||||
.filter(|_| query.role_for.as_deref() == Some(datatable_name.as_str()));
|
||||
results.push(list_one_datatable_tables(&db, &authed, &w_id, datatable_name, role).await);
|
||||
}
|
||||
|
||||
Ok(Json(results))
|
||||
}
|
||||
|
||||
async fn list_one_datatable_tables(
|
||||
db: &DB,
|
||||
authed: &ApiAuthed,
|
||||
w_id: &str,
|
||||
datatable_name: String,
|
||||
role: Option<&str>,
|
||||
) -> DataTableTables {
|
||||
let mut entry = DataTableTables {
|
||||
datatable_name,
|
||||
schemas: HashMap::new(),
|
||||
error: None,
|
||||
instance: false,
|
||||
permissioned: false,
|
||||
usable_roles: vec![],
|
||||
default_role: windmill_common::datatable_roles::ADMIN_DATATABLE_ROLE.to_string(),
|
||||
can_create_schema: false,
|
||||
creatable_schemas: vec![],
|
||||
};
|
||||
let result: Result<()> = async {
|
||||
let governing = resolve_governing_datatable(db, w_id, &entry.datatable_name).await?;
|
||||
entry.instance = governing.is_instance();
|
||||
let usable =
|
||||
crate::datatable_permissions_oss::usable_datatable_roles(db, authed, w_id, &governing)
|
||||
.await?;
|
||||
entry.permissioned = usable.permissioned;
|
||||
entry.usable_roles = usable.roles;
|
||||
entry.default_role = usable.default_role;
|
||||
let listing = get_datatable_tables(db, authed, w_id, &entry.datatable_name, role).await?;
|
||||
entry.schemas = listing.schemas;
|
||||
entry.can_create_schema = listing.can_create_schema;
|
||||
entry.creatable_schemas = listing.creatable_schemas;
|
||||
Ok(())
|
||||
}
|
||||
.await;
|
||||
if let Err(e) = result {
|
||||
entry.error = Some(e.to_string());
|
||||
}
|
||||
entry
|
||||
}
|
||||
|
||||
async fn get_datatable_table_schema(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
@@ -2475,6 +2559,7 @@ async fn get_datatable_table_schema(
|
||||
&query.datatable_name,
|
||||
&query.schema_name,
|
||||
&query.table_name,
|
||||
query.role.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -2513,14 +2598,13 @@ async fn resolve_datatable_pg_as_caller(
|
||||
authed: &ApiAuthed,
|
||||
w_id: &str,
|
||||
datatable_name: &str,
|
||||
role: Option<&str>,
|
||||
) -> Result<PgDatabase> {
|
||||
let db_resource = get_datatable_resource_from_db(
|
||||
db,
|
||||
w_id,
|
||||
datatable_name,
|
||||
// The data table's default role. Browsing has no way to name another one yet; when the
|
||||
// database manager grows a role picker it passes the pick through here.
|
||||
None,
|
||||
role,
|
||||
DatatableAccess::Authed(authed.to_authed_ref()),
|
||||
)
|
||||
.await?;
|
||||
@@ -2534,7 +2618,7 @@ async fn get_datatable_schema(
|
||||
w_id: &str,
|
||||
datatable_name: &str,
|
||||
) -> Result<SchemaMap> {
|
||||
let pg_db = resolve_datatable_pg_as_caller(db, authed, w_id, datatable_name).await?;
|
||||
let pg_db = resolve_datatable_pg_as_caller(db, authed, w_id, datatable_name, None).await?;
|
||||
|
||||
// Connect to the datatable database
|
||||
let (client, connection) = pg_db.connect(Some(db)).await?;
|
||||
@@ -2622,13 +2706,20 @@ async fn get_datatable_schema(
|
||||
Ok(schema_map)
|
||||
}
|
||||
|
||||
struct DatatableTableListing {
|
||||
schemas: TableListMap,
|
||||
can_create_schema: bool,
|
||||
creatable_schemas: Vec<String>,
|
||||
}
|
||||
|
||||
async fn get_datatable_tables(
|
||||
db: &DB,
|
||||
authed: &ApiAuthed,
|
||||
w_id: &str,
|
||||
datatable_name: &str,
|
||||
) -> Result<TableListMap> {
|
||||
let pg_db = resolve_datatable_pg_as_caller(db, authed, w_id, datatable_name).await?;
|
||||
role: Option<&str>,
|
||||
) -> Result<DatatableTableListing> {
|
||||
let pg_db = resolve_datatable_pg_as_caller(db, authed, w_id, datatable_name, role).await?;
|
||||
let (client, connection) = pg_db.connect(Some(db)).await?;
|
||||
|
||||
tokio::spawn(async move {
|
||||
@@ -2640,7 +2731,7 @@ 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_%'
|
||||
@@ -2654,11 +2745,29 @@ async fn get_datatable_tables(
|
||||
Error::internal_err(format!("Failed to query schemas: {}", pg_error_message(&e)))
|
||||
})?;
|
||||
|
||||
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 database privileges: {}",
|
||||
pg_error_message(&e)
|
||||
))
|
||||
})?
|
||||
.get(0);
|
||||
|
||||
let mut table_map: TableListMap = HashMap::new();
|
||||
let mut creatable_schemas = Vec::new();
|
||||
let schema_names: Vec<String> = schema_rows
|
||||
.iter()
|
||||
.map(|row| {
|
||||
let name: String = row.get(0);
|
||||
if row.get::<_, bool>(1) {
|
||||
creatable_schemas.push(name.clone());
|
||||
}
|
||||
table_map.entry(name.clone()).or_default();
|
||||
name
|
||||
})
|
||||
@@ -2688,7 +2797,7 @@ async fn get_datatable_tables(
|
||||
table_map.entry(table_schema).or_default().push(table_name);
|
||||
}
|
||||
|
||||
Ok(table_map)
|
||||
Ok(DatatableTableListing { schemas: table_map, can_create_schema, creatable_schemas })
|
||||
}
|
||||
|
||||
async fn get_datatable_table_columns(
|
||||
@@ -2698,6 +2807,7 @@ async fn get_datatable_table_columns(
|
||||
datatable_name: &str,
|
||||
schema_name: &str,
|
||||
table_name: &str,
|
||||
role: Option<&str>,
|
||||
) -> Result<ColumnMap> {
|
||||
if is_system_pg_schema(schema_name) {
|
||||
return Err(Error::BadRequest(format!(
|
||||
@@ -2706,7 +2816,7 @@ async fn get_datatable_table_columns(
|
||||
)));
|
||||
}
|
||||
|
||||
let pg_db = resolve_datatable_pg_as_caller(db, authed, w_id, datatable_name).await?;
|
||||
let pg_db = resolve_datatable_pg_as_caller(db, authed, w_id, datatable_name, role).await?;
|
||||
let (client, connection) = pg_db.connect(Some(db)).await?;
|
||||
|
||||
tokio::spawn(async move {
|
||||
|
||||
Reference in New Issue
Block a user