2025-09-14 16:19:44 -04:00
|
|
|
use axum::{
|
|
|
|
|
extract::{Path, State},
|
|
|
|
|
response::IntoResponse,
|
|
|
|
|
};
|
2025-11-19 01:31:09 +00:00
|
|
|
use interim_pgtypes::{
|
|
|
|
|
escape_identifier,
|
|
|
|
|
rolnames::{
|
|
|
|
|
ROLE_PREFIX_TABLE_OWNER, ROLE_PREFIX_TABLE_READER, ROLE_PREFIX_TABLE_WRITER,
|
|
|
|
|
ROLE_PREFIX_USER,
|
|
|
|
|
},
|
|
|
|
|
};
|
2025-09-14 16:19:44 -04:00
|
|
|
use serde::Deserialize;
|
|
|
|
|
use sqlx::query;
|
|
|
|
|
use uuid::Uuid;
|
|
|
|
|
|
|
|
|
|
use crate::{
|
2025-10-22 00:43:53 -07:00
|
|
|
errors::AppError,
|
2025-11-01 00:17:07 +00:00
|
|
|
navigator::{Navigator, NavigatorPage as _},
|
2025-09-14 16:19:44 -04:00
|
|
|
user::CurrentUser,
|
2025-09-23 13:08:51 -07:00
|
|
|
workspace_pooler::{RoleAssignment, WorkspacePooler},
|
2025-11-01 00:17:07 +00:00
|
|
|
workspace_utils::PHONO_TABLE_NAMESPACE,
|
2025-09-14 16:19:44 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
|
pub(super) struct PathParams {
|
|
|
|
|
workspace_id: Uuid,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// HTTP POST handler for creating a managed Postgres table within a workspace
|
|
|
|
|
/// database. Upon success, it redirects the client back to the workspace
|
|
|
|
|
/// homepage, which is expected to display a list of available tables including
|
|
|
|
|
/// the newly created one.
|
|
|
|
|
///
|
|
|
|
|
/// This handler expects 1 path parameter named `workspace_id` which should
|
|
|
|
|
/// deserialize to a UUID.
|
|
|
|
|
pub(super) async fn post(
|
|
|
|
|
State(mut pooler): State<WorkspacePooler>,
|
|
|
|
|
CurrentUser(user): CurrentUser,
|
|
|
|
|
navigator: Navigator,
|
|
|
|
|
Path(PathParams { workspace_id }): Path<PathParams>,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
2025-10-22 00:43:53 -07:00
|
|
|
// FIXME: CSRF, Check workspace authorization.
|
2025-09-14 16:19:44 -04:00
|
|
|
|
2025-10-22 00:43:53 -07:00
|
|
|
const NAME_LEN_WORDS: usize = 3;
|
|
|
|
|
let table_name = interim_namegen::default_generator()
|
|
|
|
|
.with_separator('_')
|
|
|
|
|
.generate_name(NAME_LEN_WORDS);
|
|
|
|
|
|
|
|
|
|
let mut root_client = pooler
|
2025-09-14 16:19:44 -04:00
|
|
|
// FIXME: Should this be scoped down to the unprivileged role after
|
|
|
|
|
// setting up the table owner?
|
|
|
|
|
.acquire_for(workspace_id, RoleAssignment::Root)
|
|
|
|
|
.await?;
|
|
|
|
|
|
2025-11-01 00:17:07 +00:00
|
|
|
let user_rolname = format!("{ROLE_PREFIX_USER}{uid}", uid = user.id.simple());
|
2025-10-22 00:43:53 -07:00
|
|
|
let rolname_uuid = Uuid::new_v4().simple();
|
2025-11-01 00:17:07 +00:00
|
|
|
let rolname_table_owner = format!("{ROLE_PREFIX_TABLE_OWNER}{rolname_uuid}");
|
|
|
|
|
let rolname_table_reader = format!("{ROLE_PREFIX_TABLE_READER}{rolname_uuid}");
|
|
|
|
|
let rolname_table_writer = format!("{ROLE_PREFIX_TABLE_WRITER}{rolname_uuid}");
|
2025-10-25 05:32:22 +00:00
|
|
|
for rolname in [
|
|
|
|
|
&rolname_table_owner,
|
|
|
|
|
&rolname_table_reader,
|
|
|
|
|
&rolname_table_writer,
|
|
|
|
|
] {
|
2025-10-22 00:43:53 -07:00
|
|
|
query(&format!("create role {0}", escape_identifier(rolname)))
|
|
|
|
|
.execute(root_client.get_conn())
|
|
|
|
|
.await?;
|
|
|
|
|
query(&format!(
|
|
|
|
|
"grant {0} to {1} with admin option",
|
|
|
|
|
escape_identifier(rolname),
|
|
|
|
|
escape_identifier(&user_rolname)
|
2025-09-14 16:19:44 -04:00
|
|
|
))
|
2025-10-22 00:43:53 -07:00
|
|
|
.execute(root_client.get_conn())
|
|
|
|
|
.await?;
|
|
|
|
|
}
|
2025-10-25 05:32:22 +00:00
|
|
|
|
2025-09-14 16:19:44 -04:00
|
|
|
query(&format!(
|
|
|
|
|
r#"
|
|
|
|
|
create table {0}.{1} (
|
|
|
|
|
_id uuid primary key not null default uuidv7(),
|
2025-10-01 22:36:19 -07:00
|
|
|
_created_by text default current_user,
|
2025-10-22 00:43:53 -07:00
|
|
|
_created_at timestamptz not null default now()
|
2025-09-14 16:19:44 -04:00
|
|
|
)
|
|
|
|
|
"#,
|
2025-11-01 00:17:07 +00:00
|
|
|
escape_identifier(PHONO_TABLE_NAMESPACE),
|
2025-10-22 00:43:53 -07:00
|
|
|
escape_identifier(&table_name),
|
2025-09-14 16:19:44 -04:00
|
|
|
))
|
2025-10-22 00:43:53 -07:00
|
|
|
.execute(root_client.get_conn())
|
2025-09-14 16:19:44 -04:00
|
|
|
.await?;
|
2025-10-25 05:32:22 +00:00
|
|
|
query(&format!(
|
2025-11-01 00:17:07 +00:00
|
|
|
"alter table {nsp}.{tbl} owner to {rol}",
|
|
|
|
|
nsp = escape_identifier(PHONO_TABLE_NAMESPACE),
|
|
|
|
|
tbl = escape_identifier(&table_name),
|
|
|
|
|
rol = escape_identifier(&rolname_table_owner),
|
2025-10-25 05:32:22 +00:00
|
|
|
))
|
|
|
|
|
.execute(root_client.get_conn())
|
|
|
|
|
.await?;
|
2025-09-14 16:19:44 -04:00
|
|
|
query(&format!(
|
2025-11-01 00:17:07 +00:00
|
|
|
"grant select on {nsp}.{tbl} to {rol}",
|
|
|
|
|
nsp = escape_identifier(PHONO_TABLE_NAMESPACE),
|
|
|
|
|
tbl = escape_identifier(&table_name),
|
|
|
|
|
rol = escape_identifier(&rolname_table_reader),
|
2025-09-14 16:19:44 -04:00
|
|
|
))
|
2025-10-22 00:43:53 -07:00
|
|
|
.execute(root_client.get_conn())
|
2025-09-14 16:19:44 -04:00
|
|
|
.await?;
|
2025-10-25 05:32:22 +00:00
|
|
|
query(&format!(
|
2025-11-01 00:17:07 +00:00
|
|
|
"grant delete, truncate on {nsp}.{tbl} to {rol}",
|
|
|
|
|
nsp = escape_identifier(PHONO_TABLE_NAMESPACE),
|
|
|
|
|
tbl = escape_identifier(&table_name),
|
|
|
|
|
rol = escape_identifier(&rolname_table_writer),
|
2025-10-25 05:32:22 +00:00
|
|
|
))
|
|
|
|
|
.execute(root_client.get_conn())
|
|
|
|
|
.await?;
|
2025-09-14 16:19:44 -04:00
|
|
|
|
2025-11-01 00:17:07 +00:00
|
|
|
Ok(navigator
|
|
|
|
|
.workspace_page()
|
|
|
|
|
.workspace_id(workspace_id)
|
|
|
|
|
.build()?
|
|
|
|
|
.redirect_to())
|
2025-09-14 16:19:44 -04:00
|
|
|
}
|