phonograph/interim-server/src/routes/workspaces_multi/add_handlers.rs

107 lines
3.5 KiB
Rust
Raw Normal View History

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<Settings>,
State(mut pooler): State<WorkspacePooler>,
AppDbConn(mut app_db): AppDbConn,
navigator: Navigator,
CurrentUser(user): CurrentUser,
) -> Result<impl IntoResponse, AppError> {
// 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())
}