diff --git a/phono-server/src/routes/relations_single/update_values_handler.rs b/phono-server/src/routes/relations_single/update_values_handler.rs index 1d5ab86..e73d5ae 100644 --- a/phono-server/src/routes/relations_single/update_values_handler.rs +++ b/phono-server/src/routes/relations_single/update_values_handler.rs @@ -73,7 +73,7 @@ pub(super) async fn post( .await?; // For authorization only. - let _portal = PortalAccessor::new() + PortalAccessor::new() .id(portal_id) .as_actor(Actor::User(user.id)) .verify_workspace_id(workspace_id) diff --git a/phono-server/src/routes/workspaces_single/add_table_handler.rs b/phono-server/src/routes/workspaces_single/add_table_handler.rs index 7f9191d..813f0e3 100644 --- a/phono-server/src/routes/workspaces_single/add_table_handler.rs +++ b/phono-server/src/routes/workspaces_single/add_table_handler.rs @@ -10,7 +10,7 @@ use phono_backends::{ }, }; use serde::Deserialize; -use sqlx::query; +use sqlx::{Acquire as _, query}; use uuid::Uuid; use crate::{ @@ -87,14 +87,47 @@ create table {0}.{1} ( )) .execute(root_client.get_conn()) .await?; - query(&format!( - "alter table {nsp}.{tbl} owner to {rol}", - nsp = escape_identifier(PHONO_TABLE_NAMESPACE), - tbl = escape_identifier(&table_name), - rol = escape_identifier(&rolname_table_owner), - )) - .execute(root_client.get_conn()) - .await?; + + // Postgres requires that a role have "CREATE" privileges on a schema when + // it is given ownership of a relation in that schema. This is at odds with + // our intent here, since the dedicated table owner role should never need + // nor want to have or impart the ability to create unrelated tables. + // + // While not strictly necessary, in order to keep user permissions as clean + // as possible, we run all three of the "GRANT", "ALTER", and "REVOKE" + // commands within a transaction. At least as of Postgres 18, emperical + // testing confirms that permissions updates behave similarly to + // conventional commands and queries executed within transactions, so for + // outside observers, the table owner role and its descendents should never + // appear to actually receive schema "CREATE" privileges, even momentarily, + // as a result of this code block. + { + let mut txn = root_client.get_conn().begin().await?; + query(&format!( + "grant create on schema {nsp} to {rol}", + nsp = escape_identifier(PHONO_TABLE_NAMESPACE), + rol = escape_identifier(&rolname_table_owner) + )) + .execute(&mut *txn) + .await?; + query(&format!( + "alter table {nsp}.{tbl} owner to {rol}", + nsp = escape_identifier(PHONO_TABLE_NAMESPACE), + tbl = escape_identifier(&table_name), + rol = escape_identifier(&rolname_table_owner), + )) + .execute(&mut *txn) + .await?; + query(&format!( + "revoke create on schema {nsp} from {rol}", + nsp = escape_identifier(PHONO_TABLE_NAMESPACE), + rol = escape_identifier(&rolname_table_owner) + )) + .execute(&mut *txn) + .await?; + txn.commit().await?; + } + query(&format!( "grant select on {nsp}.{tbl} to {rol}", nsp = escape_identifier(PHONO_TABLE_NAMESPACE),