106 lines
3.5 KiB
Rust
106 lines
3.5 KiB
Rust
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())
|
|
}
|