use axum::{extract::State, response::IntoResponse}; use interim_models::{ client::AppDbClient, user::User, workspace::Workspace, workspace_user_perm::WorkspaceMembership, }; use interim_pgtypes::{client::WorkspaceClient, escape_identifier}; use sqlx::{Connection as _, PgConnection, query}; use crate::{ app::AppDbConn, errors::AppError, navigator::Navigator, settings::Settings, user::CurrentUser, workspace_pooler::{RoleAssignment, WorkspacePooler}, }; /// HTTP POST handler for creating a new workspace. This handler does not expect /// any arguments, as the backing database name is generated pseudo-randomly and /// the human-friendly name may be changed later rather than specified /// permenantly upon creation. pub(super) async fn post( State(settings): State, State(mut pooler): State, AppDbConn(mut app_db): AppDbConn, navigator: Navigator, CurrentUser(user): CurrentUser, ) -> Result { // FIXME: csrf const NAME_LEN_WORDS: usize = 3; // WARNING: `db_name` is injected directly into the `create database` SQL // command. It **must not** contain spaces or any other unsafe characters. // Additionally, it **must** be URL safe without percent encoding. let db_name = interim_namegen::default_generator() .with_separator('_') .generate_name(NAME_LEN_WORDS); { // No need to pool these connections, since we don't expect to be using them // often. One less thing to keep track of in application state. let mut workspace_creator_conn = PgConnection::connect(settings.new_workspace_db_url.as_str()).await?; query(&format!( // `db_name` is an underscore-separated sequence of alphabetical words, // which should be safe to inject directly into the SQL statement. "create database {db_name}" )) .execute(&mut workspace_creator_conn) .await?; query(&format!("revoke connect on database {db_name} from public")) .execute(&mut workspace_creator_conn) .await?; // Close and drop `workspace_creator_conn` at end of block, then // reconnect using a pooled connection. workspace_creator_conn.close().await?; } let mut workspace_url = settings.new_workspace_db_url.clone(); // Alter database name but preserve auth and any query parameters. workspace_url.set_path(&db_name); let workspace = Workspace::insert() .owner_id(user.id) .url(workspace_url) .build()? .insert(&mut app_db) .await?; let mut workspace_root_conn = pooler .acquire_for(workspace.id, RoleAssignment::Root) .await?; // Initialize database user. Connection is not used and may be dropped // immediately. pooler .acquire_for(workspace.id, RoleAssignment::User(user.id)) .await?; query(&format!( "create schema {nsp}", nsp = escape_identifier(&settings.phono_table_namespace) )) .execute(workspace_root_conn.get_conn()) .await?; grant_workspace_membership( &db_name, settings.clone(), &mut app_db, &mut workspace_root_conn, &user, &workspace, ) .await?; Ok(navigator.workspace_page(workspace.id).redirect_to()) } async fn grant_workspace_membership( db_name: &str, settings: Settings, app_db_client: &mut AppDbClient, workspace_root_client: &mut WorkspaceClient, user: &User, workspace: &Workspace, ) -> Result<(), AppError> { let rolname = format!( "{prefix}{user_id}", prefix = settings.db_role_prefix, user_id = user.id.simple() ); query(&format!( "grant connect on database {db_name} to {db_user}", db_user = escape_identifier(&rolname), )) .execute(workspace_root_client.get_conn()) .await?; // TODO: There may be cases in which we will want to grant granular // workspace access which excludes privileges to create tables. query(&format!( "grant usage, create on schema {nsp} to {rolname}", nsp = escape_identifier(&settings.phono_table_namespace), rolname = escape_identifier(&rolname) )) .execute(workspace_root_client.get_conn()) .await?; WorkspaceMembership::insert() .workspace_id(workspace.id) .user_id(user.id) .build()? .execute(app_db_client) .await?; Ok(()) }