171 lines
4.9 KiB
Rust
171 lines
4.9 KiB
Rust
|
|
use std::collections::HashSet;
|
||
|
|
|
||
|
|
use anyhow::Context as _;
|
||
|
|
use askama::Template;
|
||
|
|
use axum::{
|
||
|
|
extract::{Path, State},
|
||
|
|
response::{Html, IntoResponse as _, Response},
|
||
|
|
};
|
||
|
|
use serde::Deserialize;
|
||
|
|
use sqlx::{
|
||
|
|
postgres::{types::Oid, PgRow},
|
||
|
|
query,
|
||
|
|
};
|
||
|
|
use uuid::Uuid;
|
||
|
|
|
||
|
|
use crate::{
|
||
|
|
app_error::AppError,
|
||
|
|
app_state::AppDbConn,
|
||
|
|
base_pooler::{self, BasePooler},
|
||
|
|
bases::Base,
|
||
|
|
data_layer::{Field, FieldOptionsBuilder, ToHtmlString as _, Value},
|
||
|
|
db_conns::{escape_identifier, init_role},
|
||
|
|
pg_acls::{PgAclItem, PgPrivilegeType},
|
||
|
|
pg_attributes::fetch_attributes_for_rel,
|
||
|
|
pg_classes::{PgClass, PgRelKind},
|
||
|
|
pg_roles::{PgRole, RoleTree},
|
||
|
|
settings::Settings,
|
||
|
|
users::CurrentUser,
|
||
|
|
};
|
||
|
|
|
||
|
|
#[derive(Deserialize)]
|
||
|
|
pub struct ListRelationsPagePath {
|
||
|
|
base_id: Uuid,
|
||
|
|
}
|
||
|
|
|
||
|
|
pub async fn list_relations_page(
|
||
|
|
State(Settings { base_path, .. }): State<Settings>,
|
||
|
|
State(mut base_pooler): State<BasePooler>,
|
||
|
|
AppDbConn(mut app_db): AppDbConn,
|
||
|
|
CurrentUser(current_user): CurrentUser,
|
||
|
|
Path(ListRelationsPagePath { base_id }): Path<ListRelationsPagePath>,
|
||
|
|
) -> Result<Response, AppError> {
|
||
|
|
// FIXME auth
|
||
|
|
let base = Base::fetch_by_id(base_id, &mut *app_db)
|
||
|
|
.await?
|
||
|
|
.ok_or(AppError::NotFound("no base found with that id".to_owned()))?;
|
||
|
|
let mut client = base_pooler.acquire_for(base_id).await?;
|
||
|
|
let rolname = format!("{}{}", &base.user_role_prefix, current_user.id.simple());
|
||
|
|
init_role(&rolname, &mut client).await?;
|
||
|
|
|
||
|
|
let roles = PgRole::fetch_by_names_any(vec![rolname], &mut *client).await?;
|
||
|
|
let role = roles.first().context("role not found in pg_roles")?;
|
||
|
|
let granted_role_tree = RoleTree::fetch_granted(role.oid, &mut *client)
|
||
|
|
.await?
|
||
|
|
.context("unable to construct role tree")?;
|
||
|
|
let granted_roles: HashSet<String> = granted_role_tree
|
||
|
|
.flatten_inherited()
|
||
|
|
.into_iter()
|
||
|
|
.map(|role| role.rolname.clone())
|
||
|
|
.collect();
|
||
|
|
|
||
|
|
let all_rels = PgClass::fetch_all_by_kind_any([PgRelKind::OrdinaryTable], &mut *client).await?;
|
||
|
|
let accessible_rels: Vec<PgClass> = all_rels
|
||
|
|
.into_iter()
|
||
|
|
.filter(|rel| {
|
||
|
|
let privileges: HashSet<PgPrivilegeType> = rel
|
||
|
|
.relacl
|
||
|
|
.clone()
|
||
|
|
.unwrap_or(vec![])
|
||
|
|
.into_iter()
|
||
|
|
.filter(|item| granted_roles.contains(&item.grantee))
|
||
|
|
.flat_map(|item| item.privileges)
|
||
|
|
.map(|privilege| privilege.privilege)
|
||
|
|
.collect();
|
||
|
|
privileges.contains(&PgPrivilegeType::Select)
|
||
|
|
})
|
||
|
|
.collect();
|
||
|
|
|
||
|
|
#[derive(Template)]
|
||
|
|
#[template(path = "list_rels.html")]
|
||
|
|
struct ResponseTemplate {
|
||
|
|
base_path: String,
|
||
|
|
base: Base,
|
||
|
|
rels: Vec<PgClass>,
|
||
|
|
}
|
||
|
|
|
||
|
|
Ok(Html(
|
||
|
|
ResponseTemplate {
|
||
|
|
base,
|
||
|
|
base_path,
|
||
|
|
rels: accessible_rels,
|
||
|
|
}
|
||
|
|
.render()?,
|
||
|
|
)
|
||
|
|
.into_response())
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Deserialize)]
|
||
|
|
pub struct ViewerPagePath {
|
||
|
|
base_id: Uuid,
|
||
|
|
class_oid: u32,
|
||
|
|
}
|
||
|
|
|
||
|
|
pub async fn viewer_page(
|
||
|
|
State(Settings { base_path, .. }): State<Settings>,
|
||
|
|
State(mut base_pooler): State<BasePooler>,
|
||
|
|
AppDbConn(mut app_db): AppDbConn,
|
||
|
|
CurrentUser(current_user): CurrentUser,
|
||
|
|
Path(params): Path<ViewerPagePath>,
|
||
|
|
) -> Result<Response, AppError> {
|
||
|
|
let base = Base::fetch_by_id(params.base_id, &mut *app_db)
|
||
|
|
.await?
|
||
|
|
.ok_or(AppError::NotFound("no base found with that id".to_owned()))?;
|
||
|
|
let mut client = base_pooler.acquire_for(params.base_id).await?;
|
||
|
|
|
||
|
|
init_role(
|
||
|
|
&format!("{}{}", &base.user_role_prefix, ¤t_user.id.simple()),
|
||
|
|
&mut client,
|
||
|
|
)
|
||
|
|
.await?;
|
||
|
|
|
||
|
|
// FIXME: Ensure user has access to database and relation
|
||
|
|
|
||
|
|
let class = query!(
|
||
|
|
"select relname from pg_class where oid = $1",
|
||
|
|
Oid(params.class_oid)
|
||
|
|
)
|
||
|
|
.fetch_optional(&mut *client)
|
||
|
|
.await?
|
||
|
|
.ok_or(AppError::NotFound(
|
||
|
|
"no relation found with that oid".to_owned(),
|
||
|
|
))?;
|
||
|
|
let attrs = fetch_attributes_for_rel(Oid(params.class_oid), &mut *client).await?;
|
||
|
|
|
||
|
|
const FRONTEND_ROW_LIMIT: i64 = 1000;
|
||
|
|
let rows = query(&format!(
|
||
|
|
"select {} from {} limit $1",
|
||
|
|
attrs
|
||
|
|
.iter()
|
||
|
|
.map(|attr| attr.attname.clone())
|
||
|
|
.collect::<Vec<_>>()
|
||
|
|
.join(", "),
|
||
|
|
escape_identifier(&class.relname),
|
||
|
|
))
|
||
|
|
.bind(FRONTEND_ROW_LIMIT)
|
||
|
|
.fetch_all(&mut *client)
|
||
|
|
.await?;
|
||
|
|
#[derive(Template)]
|
||
|
|
#[template(path = "class-viewer.html")]
|
||
|
|
struct ResponseTemplate {
|
||
|
|
base_path: String,
|
||
|
|
fields: Vec<Field>,
|
||
|
|
rows: Vec<PgRow>,
|
||
|
|
}
|
||
|
|
Ok(Html(
|
||
|
|
ResponseTemplate {
|
||
|
|
base_path,
|
||
|
|
fields: attrs
|
||
|
|
.into_iter()
|
||
|
|
.map(|attr| Field {
|
||
|
|
options: FieldOptionsBuilder::default().build().unwrap(),
|
||
|
|
name: attr.attname,
|
||
|
|
})
|
||
|
|
.collect(),
|
||
|
|
rows,
|
||
|
|
}
|
||
|
|
.render()?,
|
||
|
|
)
|
||
|
|
.into_response())
|
||
|
|
}
|