use axum::{extract::State, response::IntoResponse}; use interim_models::workspace::Workspace; use interim_pgtypes::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?; 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?; pooler .acquire_for(workspace.id, RoleAssignment::User(user.id)) .await?; let rolname = format!( "{prefix}{user_id}", prefix = settings.db_role_prefix, user_id = user.id.simple() ); query(&format!("revoke connect on database {db_name} from public")) .execute(&mut workspace_creator_conn) .await?; query(&format!( "grant connect on database {db_name} to {db_user}", db_user = escape_identifier(&rolname), )) .execute(&mut workspace_creator_conn) .await?; let mut workspace_root_conn = pooler .acquire_for(workspace.id, RoleAssignment::Root) .await?; query(&format!( "create schema {nsp}", nsp = escape_identifier(&settings.phono_table_namespace) )) .execute(workspace_root_conn.get_conn()) .await?; query(&format!( "grant usage, create on schema {nsp} to {rolname}", nsp = escape_identifier(&settings.phono_table_namespace), rolname = escape_identifier(&rolname) )) .execute(workspace_root_conn.get_conn()) .await?; crate::workspace_user_perms::sync_for_workspace( workspace.id, &mut app_db, &mut pooler .acquire_for(workspace.id, RoleAssignment::Root) .await?, &settings.db_role_prefix, ) .await?; Ok(navigator.workspace_page(workspace.id).redirect_to()) }