mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-19 16:03:29 +00:00
feat(datatables): permissions data model and ephemeral role lifecycle
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013fBWmrMs2DbpU1iYdCvGX6
This commit is contained in:
co-authored by
Claude Fable 5
parent
68debab877
commit
84e68472be
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS datatable_ephemeral_role;
|
||||
@@ -0,0 +1,21 @@
|
||||
-- Bookkeeping for ephemeral per-caller Postgres roles created for
|
||||
-- permissions-enabled data tables. The actual roles live on the data table's
|
||||
-- Postgres cluster; this table (on the main DB) tracks their passwords
|
||||
-- (workspace-key encrypted), the hash of the grants they were created with,
|
||||
-- and a sliding expiry.
|
||||
CREATE TABLE datatable_ephemeral_role (
|
||||
role_name TEXT PRIMARY KEY,
|
||||
workspace_id VARCHAR(50) NOT NULL REFERENCES workspace(id) ON DELETE CASCADE,
|
||||
datatable VARCHAR(255) NOT NULL,
|
||||
permissioned_as VARCHAR(255) NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
perms_hash TEXT NOT NULL,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX datatable_ephemeral_role_expires_at_idx ON datatable_ephemeral_role (expires_at);
|
||||
CREATE INDEX datatable_ephemeral_role_workspace_idx ON datatable_ephemeral_role (workspace_id, datatable);
|
||||
|
||||
GRANT ALL ON datatable_ephemeral_role TO windmill_user;
|
||||
GRANT ALL ON datatable_ephemeral_role TO windmill_admin;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -40,6 +40,7 @@ pub mod db;
|
||||
mod db_entra_ee;
|
||||
#[cfg(all(feature = "enterprise", feature = "private"))]
|
||||
mod db_iam_ee;
|
||||
pub mod datatable_permissions;
|
||||
pub mod db_params;
|
||||
#[cfg(feature = "private")]
|
||||
pub mod deployment_requests_ee;
|
||||
|
||||
@@ -989,6 +989,56 @@ pub struct DataTable {
|
||||
/// when migrations already exist (see `datatable_migrations_enabled`).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub migrations_enabled: Option<bool>,
|
||||
/// Fine-grained access control (EE). Absent = disabled: every workspace
|
||||
/// member connects through the shared role, as before the feature existed.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub permissions: Option<DataTablePermissions>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
|
||||
pub struct DataTablePermissions {
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub grants: Vec<DataTableGrant>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum DataTableFolderAccess {
|
||||
Read,
|
||||
Write,
|
||||
}
|
||||
|
||||
/// Postgres privileges grantable on data table tables. Deliberately limited to
|
||||
/// DML: DDL is owner-only in Postgres and goes through migrations, and
|
||||
/// CREATE/REFERENCES/TRIGGER would let ephemeral roles own or wire objects,
|
||||
/// fragmenting ownership away from the shared owner role.
|
||||
#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[serde(rename_all = "UPPERCASE")]
|
||||
#[derive(AsRefStr)]
|
||||
#[strum(serialize_all = "UPPERCASE")]
|
||||
pub enum DataTableOperation {
|
||||
Select,
|
||||
Insert,
|
||||
Update,
|
||||
Delete,
|
||||
Truncate,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct DataTableGrant {
|
||||
/// `u/<username>`, `g/<group>` or `f/<folder>`.
|
||||
pub tenant: String,
|
||||
/// Minimum folder access level required for the grant to apply.
|
||||
/// Only meaningful (and required) when `tenant` is a folder.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub folder_access: Option<DataTableFolderAccess>,
|
||||
pub operations: Vec<DataTableOperation>,
|
||||
pub schema: String,
|
||||
/// Empty = the whole schema, including future tables (via default privileges).
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub tables: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
@@ -1040,11 +1090,8 @@ fn datatable_not_found_error(name: &str, datatables: Option<&serde_json::Value>)
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn get_datatable_resource_from_db_unchecked(
|
||||
db: &DB,
|
||||
w_id: &str,
|
||||
name: &str,
|
||||
) -> Result<serde_json::Value> {
|
||||
/// Load a data table's config from workspace settings.
|
||||
pub async fn get_datatable_config(db: &DB, w_id: &str, name: &str) -> Result<DataTable> {
|
||||
let datatables = sqlx::query_scalar!(
|
||||
r#"
|
||||
SELECT ws.datatable->'datatables' AS datatables
|
||||
@@ -1062,8 +1109,16 @@ pub async fn get_datatable_resource_from_db_unchecked(
|
||||
.and_then(|d| d.get(name))
|
||||
.filter(|v| !v.is_null())
|
||||
.ok_or_else(|| datatable_not_found_error(name, datatables.as_ref()))?;
|
||||
let datatable = serde_json::from_value::<DataTable>(datatable.clone())?;
|
||||
Ok(serde_json::from_value::<DataTable>(datatable.clone())?)
|
||||
}
|
||||
|
||||
/// Credentials of the shared owner role for a data table: `custom_instance_user`
|
||||
/// for instance-type, the resource's own credentials for external ones.
|
||||
pub async fn datatable_shared_resource(
|
||||
db: &DB,
|
||||
w_id: &str,
|
||||
datatable: &DataTable,
|
||||
) -> Result<serde_json::Value> {
|
||||
let db_resource = if datatable.database.resource_type == DataTableCatalogResourceType::Instance
|
||||
{
|
||||
let mut pg_creds = PgDatabase::parse_uri(&get_database_url().await?.as_str().await)?;
|
||||
@@ -1084,6 +1139,15 @@ pub async fn get_datatable_resource_from_db_unchecked(
|
||||
Ok(db_resource)
|
||||
}
|
||||
|
||||
pub async fn get_datatable_resource_from_db_unchecked(
|
||||
db: &DB,
|
||||
w_id: &str,
|
||||
name: &str,
|
||||
) -> Result<serde_json::Value> {
|
||||
let datatable = get_datatable_config(db, w_id, name).await?;
|
||||
datatable_shared_resource(db, w_id, &datatable).await
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct Ducklake {
|
||||
pub catalog: DucklakeCatalog,
|
||||
|
||||
Reference in New Issue
Block a user