diff --git a/Cargo.lock b/Cargo.lock index 0163ef0..8d5c7ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1757,6 +1757,7 @@ dependencies = [ "bigdecimal", "chrono", "derive_builder", + "futures", "interim-pgtypes", "redact", "regex", @@ -1765,6 +1766,7 @@ dependencies = [ "sqlx", "strum", "thiserror 2.0.12", + "tracing", "url", "uuid", "validator", diff --git a/interim-models/Cargo.toml b/interim-models/Cargo.toml index c336562..f62c642 100644 --- a/interim-models/Cargo.toml +++ b/interim-models/Cargo.toml @@ -7,6 +7,7 @@ version.workspace = true bigdecimal = { workspace = true } chrono = { workspace = true } derive_builder = { workspace = true } +futures = { workspace = true } interim-pgtypes = { path = "../interim-pgtypes" } redact = { workspace = true } regex = { workspace = true } @@ -15,6 +16,7 @@ serde_json = { workspace = true } sqlx = { workspace = true } strum = { workspace = true } thiserror = { workspace = true } +tracing = { workspace = true } url = { workspace = true } uuid = { workspace = true } validator = { workspace = true } diff --git a/interim-models/src/accessors/mod.rs b/interim-models/src/accessors/mod.rs new file mode 100644 index 0000000..202719c --- /dev/null +++ b/interim-models/src/accessors/mod.rs @@ -0,0 +1,53 @@ +use std::sync::Arc; + +use derive_builder::UninitializedFieldError; +use uuid::Uuid; + +pub mod portal; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Actor { + /// Bypass explicit auth checks. + Bypass, + + User(Uuid), +} + +/// Encompasses all possible (non-fatal) errors encountered while executing an +/// [`Accessor`]'s `fetch_one()` or `fetch_optional()` methods. +#[derive(Clone, Debug, thiserror::Error)] +pub enum AccessError { + #[error("database error: {0}")] + Database(Arc), + + #[error("not found or access denied")] + NotFound, + + #[error("incomplete access spec: {0}")] + Incomplete(UninitializedFieldError), +} + +impl From for AccessError { + fn from(value: sqlx::Error) -> Self { + Self::Database(Arc::new(value)) + } +} + +impl From for AccessError { + fn from(value: UninitializedFieldError) -> Self { + Self::Incomplete(value) + } +} + +/// Provides methods for fetching database entities of type `T`, typically with +/// authorization checks. +pub trait Accessor: Default + Sized { + /// Returns a new builder struct. Alias for [`Self::default()`]. + fn new() -> Self { + Self::default() + } + + /// Fetch an entity from the database, returning [`AccessError::NotFound`] + /// if access is denied or the object cannot be found. + fn fetch_one(self) -> impl Future>; +} diff --git a/interim-models/src/accessors/portal.rs b/interim-models/src/accessors/portal.rs new file mode 100644 index 0000000..89abe47 --- /dev/null +++ b/interim-models/src/accessors/portal.rs @@ -0,0 +1,202 @@ +use std::collections::HashSet; + +use derive_builder::Builder; +use interim_pgtypes::{ + client::WorkspaceClient, pg_acl::PgPrivilegeType, pg_class::PgClass, pg_role::RoleTree, + rolnames::ROLE_PREFIX_USER, +}; +use sqlx::postgres::types::Oid; +use tracing::{Instrument, info, info_span}; +use uuid::Uuid; + +use crate::{client::AppDbClient, portal::Portal}; + +use super::{AccessError, Accessor, Actor}; + +/// Utility for fetching a [`Portal`], with authorization. +#[derive(Builder, Debug)] +#[builder( + // Build fn should only be called internally, via `fetch_optional()`. + build_fn(private, error = "super::AccessError"), + // Callers interact primarily with the generated builder struct. + name = "PortalAccessor", + vis = "pub", + // "Owned" pattern circumvents the `Clone` trait bound on fields. + pattern = "owned", +)] +struct GeneratedPortalAccessor<'a> { + /// Required. ID of the portal to be accessed. + id: Uuid, + + /// Required. Identity against which to evaluate authorization checks. + as_actor: Actor, + + /// Required. Client for the backing database. + using_workspace_client: &'a mut WorkspaceClient, + + /// Required. Client for the application database. + using_app_db: &'a mut AppDbClient, + + /// Optionally verify that the portal is associated with a specific + /// workspace ID. + #[builder(default, setter(strip_option))] + verify_workspace_id: Option, + + /// Optionally verify that the portal is associated with a specific relation + /// OID. + #[builder(default, setter(strip_option))] + verify_rel_oid: Option, + + /// Optionally verify that the actor has or inherits specific Postgres + /// permissions on the relation in the backing database. + #[builder(default, setter(into))] + // Using [`HashSet`] with `#[builder(setter(into))]` is + // more straightforward for callers than asking them to provide a + // `&'a [PgPrivilege]`. Forcing a conversion to a heap-based data structure + // feels wrong when callers will often be starting with a statically + // allocated array of `PgPrivilegeType`s, but that optimization can be made + // later if the trade-off with ergonomics proves warranted. + verify_rel_permissions: HashSet, + + // Find doc comment on setter method. + #[builder(default, setter(custom))] + verify_rel_ownership: bool, + + /// Optionally for performance, relation metadata may be provided by the + /// caller so that the accessor doesn't need to refetch it behind the + /// scenes. + #[builder(default, setter(strip_option))] + using_rel: Option<&'a PgClass>, +} + +impl PortalAccessor<'_> { + /// Optionally verify that the actor has or belongs to the role that owns + /// the relation in the backing database. + pub fn verify_rel_ownership(self) -> Self { + Self { + verify_rel_ownership: Some(true), + ..self + } + } +} + +impl<'a> Accessor for PortalAccessor<'a> { + async fn fetch_one(self) -> Result { + let spec = self.build()?; + async { + // FIXME: Do we need to explicitly verify that the actor has + // database `CONNECT` privileges, or is that implicitly given by the + // fact that a workspace client has already been acquired? + + let portal = Portal::with_id(spec.id) + .fetch_optional(spec.using_app_db) + .await? + .ok_or(AccessError::NotFound)?; + + spec.verify_workspace_id + .is_none_or(|value| portal.workspace_id == value) + .ok_or_else(|| { + info!("workspace_id check failed for portal"); + AccessError::NotFound + })?; + spec.verify_rel_oid + .is_none_or(|value| portal.class_oid == value) + .ok_or_else(|| { + info!("rel_oid check failed for portal"); + AccessError::NotFound + })?; + + let rel = if let Some(value) = spec.using_rel { + value + } else { + &PgClass::with_oid(portal.class_oid) + .fetch_optional(spec.using_workspace_client) + .await? + .ok_or_else(|| { + info!("unable to fetch PgClass for portal"); + AccessError::NotFound + })? + }; + + let actor_rolname = match spec.as_actor { + Actor::Bypass => None, + Actor::User(user_id) => Some(format!( + "{ROLE_PREFIX_USER}{user_id}", + user_id = user_id.simple() + )), + }; + + if let Some(actor_rolname) = actor_rolname { + // Verify ACL permissions. + // + // No need to explicitly check whether + // `verify_rel_permissions` is empty: database queries are + // only performed based on roles which intersect with the + // permissions check. + // + // This could alternatively be implemented using streams, + // but this naive for-loop version is much more readable and + // still bottlenecks on the same serial database access. + let mut actor_permissions: HashSet = HashSet::new(); + for acl_item in rel + .relacl + .as_ref() + .unwrap_or(&vec![]) + .iter() + .filter(|acl_item| { + acl_item + .privileges + .iter() + .any(|value| spec.verify_rel_permissions.contains(&value.privilege)) + }) + { + // The naive equality check is technically redundant, + // but it allows us to eliminate a database round trip + // if it evaluates to true. + if acl_item.grantee == actor_rolname + || RoleTree::members_of_rolname(&acl_item.grantee) + .fetch_tree(spec.using_workspace_client) + .await? + .map(|tree| tree.flatten_inherited()) + .unwrap_or_default() + .iter() + .any(|value| value.rolname == actor_rolname) + { + for permission in acl_item.privileges.iter() { + actor_permissions.insert(permission.privilege); + } + } + } + if !actor_permissions.is_superset(&spec.verify_rel_permissions) { + info!("actor lacks postgres privileges"); + return Err(AccessError::NotFound); + } + + // Verify relation ownership. + if spec.verify_rel_ownership + // The naive equality check is technically redundant, + // but it allows us to eliminate a database round trip + // if it evaluates to true. + && (rel.regowner == actor_rolname + || !RoleTree::members_of_oid(rel.relowner) + .fetch_tree(spec.using_workspace_client) + .await? + .map(|tree| tree.flatten_inherited()) + .unwrap_or_default() + .iter() + .any(|value| value.rolname == actor_rolname)) + { + info!("actor is not relation owner"); + return Err(AccessError::NotFound); + } + } + + Ok(portal) + } + .instrument(info_span!( + "PortalAccessor::fetch_optional()", + portal_id = spec.id.to_string(), + )) + .await + } +} diff --git a/interim-models/src/lib.rs b/interim-models/src/lib.rs index 7b4796d..a5a5d4b 100644 --- a/interim-models/src/lib.rs +++ b/interim-models/src/lib.rs @@ -1,3 +1,6 @@ +#![feature(bool_to_result)] // Enable support for `ok_or()` on bools. + +pub mod accessors; pub mod client; pub mod cluster; pub mod datum; diff --git a/interim-pgtypes/src/client.rs b/interim-pgtypes/src/client.rs index 4b1fcf8..704fc45 100644 --- a/interim-pgtypes/src/client.rs +++ b/interim-pgtypes/src/client.rs @@ -4,8 +4,9 @@ use crate::escape_identifier; /// Newtype to differentiate between workspace and application database /// connections. +#[derive(Debug)] pub struct WorkspaceClient { - pub(crate) conn: PoolConnection, + conn: PoolConnection, } impl WorkspaceClient { diff --git a/interim-pgtypes/src/lib.rs b/interim-pgtypes/src/lib.rs index f37d9e8..6d3aa4d 100644 --- a/interim-pgtypes/src/lib.rs +++ b/interim-pgtypes/src/lib.rs @@ -5,6 +5,7 @@ pub mod pg_class; pub mod pg_database; pub mod pg_namespace; pub mod pg_role; +pub mod rolnames; /// Given a raw identifier (such as a table name, column name, etc.), format it /// so that it may be safely interpolated into a SQL query. diff --git a/interim-pgtypes/src/pg_attribute.rs b/interim-pgtypes/src/pg_attribute.rs index 09681d8..8b1550d 100644 --- a/interim-pgtypes/src/pg_attribute.rs +++ b/interim-pgtypes/src/pg_attribute.rs @@ -88,7 +88,7 @@ where attrelid = $1 and attnum > 0 and not attisdropped "#, &self.rel_oid ) - .fetch_all(&mut *client.conn) + .fetch_all(client.get_conn()) .await } } @@ -131,7 +131,7 @@ where i.indrelid = $1 and i.indisprimary; "#, &self.rel_oid ) - .fetch_all(&mut *client.conn) + .fetch_all(client.get_conn()) .await } } diff --git a/interim-pgtypes/src/pg_class.rs b/interim-pgtypes/src/pg_class.rs index 48fbc92..6c80cde 100644 --- a/interim-pgtypes/src/pg_class.rs +++ b/interim-pgtypes/src/pg_class.rs @@ -52,7 +52,7 @@ impl PgClass { &self, client: &mut WorkspaceClient, ) -> Result { - PgNamespace::fetch_by_oid(self.relnamespace, &mut *client.conn) + PgNamespace::fetch_by_oid(self.relnamespace, client.get_conn()) .await? // If client has access to the class, it would expect to have access // to the namespace that contains it. If not, that's an error. @@ -178,7 +178,7 @@ where impl WithOidQuery { pub async fn fetch_one(self, client: &mut WorkspaceClient) -> Result { with_oid_sqlx_query!(self.oid) - .fetch_one(&mut *client.conn) + .fetch_one(client.get_conn()) .await } @@ -187,7 +187,7 @@ impl WithOidQuery { client: &mut WorkspaceClient, ) -> Result, sqlx::Error> { with_oid_sqlx_query!(self.oid) - .fetch_optional(&mut *client.conn) + .fetch_optional(client.get_conn()) .await } } @@ -235,7 +235,7 @@ where "#, kinds_i8.as_slice(), ) - .fetch_all(&mut *client.conn) + .fetch_all(client.get_conn()) .await } } diff --git a/interim-pgtypes/src/pg_database.rs b/interim-pgtypes/src/pg_database.rs index 0423ab9..7310731 100644 --- a/interim-pgtypes/src/pg_database.rs +++ b/interim-pgtypes/src/pg_database.rs @@ -73,7 +73,7 @@ from pg_database where datname = current_database() "#, ) - .fetch_one(&mut *client.conn) + .fetch_one(client.get_conn()) .await } } diff --git a/interim-pgtypes/src/pg_role.rs b/interim-pgtypes/src/pg_role.rs index 2b165b2..744f25f 100644 --- a/interim-pgtypes/src/pg_role.rs +++ b/interim-pgtypes/src/pg_role.rs @@ -9,24 +9,34 @@ use crate::client::WorkspaceClient; pub struct PgRole { /// ID of role pub oid: Oid, + /// Role name pub rolname: String, + /// Role has superuser privileges pub rolsuper: bool, + /// Role automatically inherits privileges of roles it is a member of pub rolinherit: bool, + /// Role can create more roles pub rolcreaterole: bool, + /// Role can create databases pub rolcreatedb: bool, + /// Role can log in. That is, this role can be given as the initial session authorization identifier pub rolcanlogin: bool, + /// Role is a replication role. A replication role can initiate replication connections and create and drop replication slots. pub rolreplication: bool, + /// For roles that can log in, this sets maximum number of concurrent connections this role can make. -1 means no limit. pub rolconnlimit: i32, + /// Password expiry time (only used for password authentication); null if no expiration pub rolvaliduntil: Option>, + /// Role bypasses every row-level security policy, see Section 5.9 for more information. pub rolbypassrls: bool, } @@ -71,7 +81,7 @@ where rolname = any($1) "#, self.names.as_slice() ) - .fetch_all(&mut *client.conn) + .fetch_all(client.get_conn()) .await } } @@ -106,11 +116,16 @@ where starts_with(rolname, $1) "#, self.prefix ) - .fetch_all(&mut *client.conn) + .fetch_all(client.get_conn()) .await } } +/// A representation of role grants, starting from a single role and traversing +/// the full set (as visible via the current DB connection) of either more +/// specific roles which are granted to it, or more general roles to which it +/// is granted. This is useful, for example, for enumerating permissions which +/// are inherited rather than granted directly to a specific user. #[derive(Clone, Debug)] pub struct RoleTree { pub role: PgRole, @@ -187,7 +202,7 @@ impl MembersOfOidQuery { ", ) .bind(self.role) - .fetch_all(&mut *client.conn) + .fetch_all(client.get_conn()) .await?; Ok(rows .iter() @@ -231,7 +246,7 @@ impl MembersOfRolnameQuery { ", ) .bind(self.role.as_str() as &str) - .fetch_all(&mut *client.conn) + .fetch_all(client.get_conn()) .await?; Ok(rows .iter() @@ -273,7 +288,7 @@ from ( ", ) .bind(self.role_oid) - .fetch_all(&mut *client.conn) + .fetch_all(client.get_conn()) .await?; Ok(rows .iter() @@ -315,7 +330,7 @@ from ( ", ) .bind(self.rolname) - .fetch_all(&mut *client.conn) + .fetch_all(client.get_conn()) .await?; Ok(rows .iter() diff --git a/interim-pgtypes/src/rolnames.rs b/interim-pgtypes/src/rolnames.rs new file mode 100644 index 0000000..a4e95d7 --- /dev/null +++ b/interim-pgtypes/src/rolnames.rs @@ -0,0 +1,7 @@ +pub const ROLE_PREFIX_SERVICE_CRED: &str = "svc_"; +pub const ROLE_PREFIX_TABLE_OWNER: &str = "tbo_"; +pub const ROLE_PREFIX_TABLE_READER: &str = "tbr_"; +pub const ROLE_PREFIX_TABLE_WRITER: &str = "tbw_"; +pub const ROLE_PREFIX_USER: &str = "usr_"; +pub const SERVICE_CRED_SUFFIX_LEN: usize = 8; +pub const SERVICE_CRED_CONN_LIMIT: usize = 4; diff --git a/interim-server/src/errors.rs b/interim-server/src/errors.rs index 487a939..35e29be 100644 --- a/interim-server/src/errors.rs +++ b/interim-server/src/errors.rs @@ -5,31 +5,31 @@ use axum::response::{IntoResponse, Response}; macro_rules! forbidden { ($message:literal) => { - AppError::Forbidden($message.to_owned()) + crate::errors::AppError::Forbidden($message.to_owned()) }; ($message:literal, $($param:expr),+) => { - AppError::Forbidden(format!($message, $($param)+)) + crate::errors::AppError::Forbidden(format!($message, $($param)+)) }; } macro_rules! not_found { ($message:literal) => { - AppError::NotFound($message.to_owned()) + crate::errors::AppError::NotFound($message.to_owned()) }; ($message:literal, $($param:expr),+) => { - AppError::NotFound(format!($message, $($param)+)) + crate::errors::AppError::NotFound(format!($message, $($param)+)) }; } macro_rules! bad_request { ($message:literal) => { - AppError::BadRequest(format!($message)) + crate::errors::AppError::BadRequest(format!($message)) }; ($message:literal, $($param:expr),+) => { - AppError::BadRequest(format!($message, $($param)+)) + crate::errors::AppError::BadRequest(format!($message, $($param)+)) }; } diff --git a/interim-server/src/roles.rs b/interim-server/src/roles.rs index 3993a3f..c176244 100644 --- a/interim-server/src/roles.rs +++ b/interim-server/src/roles.rs @@ -6,20 +6,13 @@ use interim_pgtypes::{ client::WorkspaceClient, pg_acl::{PgAclItem, PgPrivilegeType}, pg_class::PgClass, + rolnames::{ROLE_PREFIX_TABLE_OWNER, ROLE_PREFIX_TABLE_READER, ROLE_PREFIX_TABLE_WRITER}, }; use serde::{Deserialize, Serialize}; use sqlx::{postgres::types::Oid, prelude::FromRow, query_as}; use crate::errors::AppError; -pub(crate) const ROLE_PREFIX_SERVICE_CRED: &str = "svc_"; -pub(crate) const ROLE_PREFIX_TABLE_OWNER: &str = "tbo_"; -pub(crate) const ROLE_PREFIX_TABLE_READER: &str = "tbr_"; -pub(crate) const ROLE_PREFIX_TABLE_WRITER: &str = "tbw_"; -pub(crate) const ROLE_PREFIX_USER: &str = "usr_"; -pub(crate) const SERVICE_CRED_SUFFIX_LEN: usize = 8; -pub(crate) const SERVICE_CRED_CONN_LIMIT: usize = 4; - // TODO: custom error type // TODO: make params and result references fn get_table_role( diff --git a/interim-server/src/routes/forms/form_handler.rs b/interim-server/src/routes/forms/form_handler.rs index 0c12a26..ceecc8c 100644 --- a/interim-server/src/routes/forms/form_handler.rs +++ b/interim-server/src/routes/forms/form_handler.rs @@ -12,7 +12,7 @@ use interim_models::{ portal::Portal, presentation::{Presentation, TextInputMode}, }; -use interim_pgtypes::{pg_attribute::PgAttribute, pg_class::PgClass}; +use interim_pgtypes::pg_attribute::PgAttribute; use serde::Deserialize; use uuid::Uuid; @@ -29,19 +29,22 @@ pub(super) struct PathParams { portal_id: Uuid, } +/// HTTP GET handler for public-facing survey interface. This allows form +/// responses to be collected as rows directly into a Phonograph table. pub(super) async fn get( State(settings): State, State(mut pooler): State, AppDbConn(mut app_db): AppDbConn, Path(PathParams { portal_id }): Path, ) -> Result { + // FIXME: Disallow access unless form has been explicitly marked as public. + + // WARNING: Form handler bypasses standard auth checks. let portal = Portal::with_id(portal_id) .fetch_optional(&mut app_db) .await? .ok_or(not_found!("form not found"))?; - // FIXME: auth - // WARNING: This client is connected with full workspace privileges. Even // more so than usual, the Phonograph server is responsible for ensuring all // auth checks are performed properly. @@ -52,9 +55,6 @@ pub(super) async fn get( .acquire_for(portal.workspace_id, RoleAssignment::Root) .await?; - let rel = PgClass::with_oid(portal.class_oid) - .fetch_one(&mut workspace_client) - .await?; let attrs: HashMap = PgAttribute::all_for_rel(portal.class_oid) .fetch_all(&mut workspace_client) .await? diff --git a/interim-server/src/routes/relations_single/add_field_handler.rs b/interim-server/src/routes/relations_single/add_field_handler.rs index 1fcf26d..55e8e1e 100644 --- a/interim-server/src/routes/relations_single/add_field_handler.rs +++ b/interim-server/src/routes/relations_single/add_field_handler.rs @@ -7,7 +7,9 @@ use axum::{ // https://docs.rs/axum-extra/0.10.1/axum_extra/extract/struct.Form.html#differences-from-axumextractform use axum_extra::extract::Form; use interim_models::{ - field::Field, portal::Portal, presentation::Presentation, workspace::Workspace, + accessors::{Accessor as _, Actor, portal::PortalAccessor}, + field::Field, + presentation::Presentation, }; use interim_pgtypes::{escape_identifier, pg_class::PgClass}; use serde::Deserialize; @@ -49,7 +51,7 @@ pub(super) struct FormBody { /// [`PathParams`]. #[debug_handler(state = App)] pub(super) async fn post( - State(mut workspace_pooler): State, + State(mut pooler): State, AppDbConn(mut app_db): AppDbConn, CurrentUser(user): CurrentUser, navigator: Navigator, @@ -60,28 +62,33 @@ pub(super) async fn post( }): Path, Form(form): Form, ) -> Result { - // FIXME: Check workspace authorization. - // FIXME ensure workspace corresponds to rel/portal, and that user has - // permission to access/alter both as needed. + // FIXME: csrf - let portal = Portal::with_id(portal_id).fetch_one(&mut app_db).await?; - let workspace = Workspace::with_id(portal.workspace_id) - .fetch_one(&mut app_db) + let mut workspace_client = pooler + .acquire_for(workspace_id, RoleAssignment::User(user.id)) .await?; - let mut workspace_client = workspace_pooler - .acquire_for(workspace.id, RoleAssignment::User(user.id)) - .await?; - - let class = PgClass::with_oid(portal.class_oid) + let rel = PgClass::with_oid(Oid(rel_oid)) .fetch_one(&mut workspace_client) .await?; + let portal = PortalAccessor::new() + .id(portal_id) + .as_actor(Actor::User(user.id)) + .verify_workspace_id(workspace_id) + .verify_rel_oid(Oid(rel_oid)) + .verify_rel_ownership() + .using_rel(&rel) + .using_app_db(&mut app_db) + .using_workspace_client(&mut workspace_client) + .fetch_one() + .await?; + let presentation = Presentation::try_from(form.presentation_form)?; query(&format!( "alter table {ident} add column if not exists {col} {typ}", - ident = class.get_identifier(), + ident = rel.get_identifier(), col = escape_identifier(&form.name), typ = presentation.attr_data_type_fragment(), )) @@ -90,8 +97,8 @@ pub(super) async fn post( query(&format!( "grant insert ({col}), update ({col}) on table {ident} to {writer_role}", col = escape_identifier(&form.name), - ident = class.get_identifier(), - writer_role = escape_identifier(&get_writer_role(&class)?), + ident = rel.get_identifier(), + writer_role = escape_identifier(&get_writer_role(&rel)?), )) .execute(workspace_client.get_conn()) .await?; diff --git a/interim-server/src/routes/relations_single/form_handler.rs b/interim-server/src/routes/relations_single/form_handler.rs index 7daecca..2f0e96b 100644 --- a/interim-server/src/routes/relations_single/form_handler.rs +++ b/interim-server/src/routes/relations_single/form_handler.rs @@ -7,8 +7,13 @@ use axum::{ response::{Html, IntoResponse}, }; use interim_models::{ - field::Field, field_form_prompt::FieldFormPrompt, form_transition::FormTransition, - language::Language, portal::Portal, workspace::Workspace, + accessors::{Accessor as _, Actor, portal::PortalAccessor}, + field::Field, + field_form_prompt::FieldFormPrompt, + form_transition::FormTransition, + language::Language, + portal::Portal, + workspace::Workspace, }; use interim_pgtypes::pg_attribute::PgAttribute; use serde::{Deserialize, Serialize}; @@ -49,17 +54,23 @@ pub(super) async fn get( navigator: Navigator, State(mut pooler): State, ) -> Result { - // FIXME: Check workspace authorization. - // FIXME ensure workspace corresponds to rel/portal, and that user has - // permission to access/alter both as needed. - - let portal = Portal::with_id(portal_id).fetch_one(&mut app_db).await?; - let workspace = Workspace::with_id(portal.workspace_id) - .fetch_one(&mut app_db) + let mut workspace_client = pooler + .acquire_for(workspace_id, RoleAssignment::User(user.id)) .await?; - let mut workspace_client = pooler - .acquire_for(workspace.id, RoleAssignment::User(user.id)) + let portal = PortalAccessor::new() + .id(portal_id) + .as_actor(Actor::User(user.id)) + .verify_workspace_id(workspace_id) + .verify_rel_oid(Oid(rel_oid)) + .verify_rel_ownership() + .using_app_db(&mut app_db) + .using_workspace_client(&mut workspace_client) + .fetch_one() + .await?; + + let workspace = Workspace::with_id(portal.workspace_id) + .fetch_one(&mut app_db) .await?; let attrs: HashMap = PgAttribute::all_for_rel(portal.class_oid) diff --git a/interim-server/src/routes/relations_single/get_data_handler.rs b/interim-server/src/routes/relations_single/get_data_handler.rs index e9c4355..4e765fc 100644 --- a/interim-server/src/routes/relations_single/get_data_handler.rs +++ b/interim-server/src/routes/relations_single/get_data_handler.rs @@ -5,10 +5,19 @@ use axum::{ extract::{Path, State}, response::{IntoResponse as _, Response}, }; -use interim_models::{datum::Datum, field::Field, portal::Portal}; -use interim_pgtypes::{escape_identifier, pg_attribute::PgAttribute, pg_class::PgClass}; +use interim_models::{ + accessors::{Accessor, Actor, portal::PortalAccessor}, + datum::Datum, + field::Field, +}; +use interim_pgtypes::{ + escape_identifier, pg_acl::PgPrivilegeType, pg_attribute::PgAttribute, pg_class::PgClass, +}; use serde::{Deserialize, Serialize}; -use sqlx::{postgres::PgRow, query}; +use sqlx::{ + postgres::{PgRow, types::Oid}, + query, +}; use uuid::Uuid; use crate::{ @@ -22,6 +31,8 @@ use crate::{ #[derive(Clone, Debug, Deserialize)] pub(super) struct PathParams { portal_id: Uuid, + rel_oid: u32, + workspace_id: Uuid, } const FRONTEND_ROW_LIMIT: i64 = 1000; @@ -33,19 +44,33 @@ const FRONTEND_ROW_LIMIT: i64 = 1000; pub(super) async fn get( State(mut workspace_pooler): State, AppDbConn(mut app_db): AppDbConn, - CurrentUser(current_user): CurrentUser, - Path(PathParams { portal_id }): Path, + CurrentUser(user): CurrentUser, + Path(PathParams { + portal_id, + rel_oid, + workspace_id, + }): Path, ) -> Result { - // FIXME auth - let portal = Portal::with_id(portal_id).fetch_one(&mut app_db).await?; - let mut workspace_client = workspace_pooler - .acquire_for(portal.workspace_id, RoleAssignment::User(current_user.id)) + .acquire_for(workspace_id, RoleAssignment::User(user.id)) .await?; - let rel = PgClass::with_oid(portal.class_oid) + + let rel = PgClass::with_oid(Oid(rel_oid)) .fetch_one(&mut workspace_client) .await?; + let portal = PortalAccessor::default() + .id(portal_id) + .as_actor(Actor::User(user.id)) + .verify_workspace_id(workspace_id) + .verify_rel_oid(Oid(rel_oid)) + .verify_rel_permissions([PgPrivilegeType::Select]) + .using_rel(&rel) + .using_workspace_client(&mut workspace_client) + .using_app_db(&mut app_db) + .fetch_one() + .await?; + let attrs = PgAttribute::all_for_rel(portal.class_oid) .fetch_all(&mut workspace_client) .await?; diff --git a/interim-server/src/routes/relations_single/insert_handler.rs b/interim-server/src/routes/relations_single/insert_handler.rs index 19e277b..f8deedd 100644 --- a/interim-server/src/routes/relations_single/insert_handler.rs +++ b/interim-server/src/routes/relations_single/insert_handler.rs @@ -8,8 +8,11 @@ use axum::{ // [`axum_extra`]'s form extractor is required to support repeated keys: // https://docs.rs/axum-extra/0.10.1/axum_extra/extract/struct.Form.html#differences-from-axumextractform use axum_extra::extract::Form; -use interim_models::{datum::Datum, portal::Portal, workspace::Workspace}; -use interim_pgtypes::{escape_identifier, pg_class::PgClass}; +use interim_models::{ + accessors::{Accessor as _, Actor, portal::PortalAccessor}, + datum::Datum, +}; +use interim_pgtypes::{escape_identifier, pg_acl::PgPrivilegeType, pg_class::PgClass}; use serde::Deserialize; use sqlx::{postgres::types::Oid, query}; use uuid::Uuid; @@ -47,24 +50,29 @@ pub(super) async fn post( }): Path, Form(form): Form>>, ) -> Result { - // FIXME: Check workspace authorization. - // FIXME ensure workspace corresponds to rel/portal, and that user has - // permission to access/alter both as needed. // FIXME CSRF - let portal = Portal::with_id(portal_id).fetch_one(&mut app_db).await?; - let workspace = Workspace::with_id(portal.workspace_id) - .fetch_one(&mut app_db) - .await?; - let mut workspace_client = workspace_pooler - .acquire_for(workspace.id, RoleAssignment::User(user.id)) + .acquire_for(workspace_id, RoleAssignment::User(user.id)) .await?; let rel = PgClass::with_oid(Oid(rel_oid)) .fetch_one(&mut workspace_client) .await?; + // For authorization only. + let _portal = PortalAccessor::new() + .id(portal_id) + .as_actor(Actor::User(user.id)) + .verify_workspace_id(workspace_id) + .verify_rel_oid(Oid(rel_oid)) + .verify_rel_permissions([PgPrivilegeType::Insert]) + .using_rel(&rel) + .using_app_db(&mut app_db) + .using_workspace_client(&mut workspace_client) + .fetch_one() + .await?; + let col_names: Vec = form.keys().cloned().collect(); // Prevent users from modifying Phonograph metadata columns. diff --git a/interim-server/src/routes/relations_single/portal_handler.rs b/interim-server/src/routes/relations_single/portal_handler.rs index 0ab6af9..431d4f5 100644 --- a/interim-server/src/routes/relations_single/portal_handler.rs +++ b/interim-server/src/routes/relations_single/portal_handler.rs @@ -3,8 +3,12 @@ use axum::{ extract::{Path, State}, response::{Html, IntoResponse as _, Response}, }; -use interim_models::{expression::PgExpressionAny, portal::Portal, workspace::Workspace}; -use interim_pgtypes::pg_attribute::PgAttribute; +use interim_models::{ + accessors::{Accessor, Actor, portal::PortalAccessor}, + expression::PgExpressionAny, + workspace::Workspace, +}; +use interim_pgtypes::{pg_acl::PgPrivilegeType, pg_attribute::PgAttribute}; use serde::{Deserialize, Serialize}; use sqlx::postgres::types::Oid; use uuid::Uuid; @@ -19,7 +23,7 @@ use crate::{ workspace_pooler::{RoleAssignment, WorkspacePooler}, }; -#[derive(Clone, Debug, Deserialize)] +#[derive(Debug, Deserialize)] pub(super) struct PathParams { portal_id: Uuid, rel_oid: u32, @@ -32,25 +36,29 @@ pub(super) struct PathParams { /// module. pub(super) async fn get( State(settings): State, - State(mut workspace_pooler): State, + State(mut pooler): State, AppDbConn(mut app_db): AppDbConn, - CurrentUser(current_user): CurrentUser, + CurrentUser(user): CurrentUser, navigator: Navigator, Path(PathParams { portal_id, - workspace_id, rel_oid, + workspace_id, }): Path, ) -> Result { - // FIXME auth - - let workspace = Workspace::with_id(workspace_id) - .fetch_one(&mut app_db) + let mut workspace_client = pooler + .acquire_for(workspace_id, RoleAssignment::User(user.id)) .await?; - let portal = Portal::with_id(portal_id).fetch_one(&mut app_db).await?; - let mut workspace_client = workspace_pooler - .acquire_for(portal.workspace_id, RoleAssignment::User(current_user.id)) + let portal = PortalAccessor::new() + .id(portal_id) + .as_actor(Actor::User(user.id)) + .verify_workspace_id(workspace_id) + .verify_rel_oid(Oid(rel_oid)) + .verify_rel_permissions([PgPrivilegeType::Select]) + .using_app_db(&mut app_db) + .using_workspace_client(&mut workspace_client) + .fetch_one() .await?; let attrs = PgAttribute::all_for_rel(portal.class_oid) @@ -70,6 +78,10 @@ pub(super) async fn get( }) .collect(); + let workspace = Workspace::with_id(portal.workspace_id) + .fetch_one(&mut app_db) + .await?; + #[derive(Template)] #[template(path = "portal_table.html")] struct ResponseTemplate { @@ -86,11 +98,11 @@ pub(super) async fn get( filter: portal.table_filter.0, navbar: WorkspaceNav::builder() .navigator(navigator) - .workspace(workspace.clone()) + .workspace(workspace) .populate_rels(&mut app_db, &mut workspace_client) .await? .current(NavLocation::Rel( - Oid(rel_oid), + portal.class_oid, Some(RelLocation::Portal(portal.id)), )) .build()?, diff --git a/interim-server/src/routes/relations_single/portal_settings_handler.rs b/interim-server/src/routes/relations_single/portal_settings_handler.rs index 4c2e929..07ba88d 100644 --- a/interim-server/src/routes/relations_single/portal_settings_handler.rs +++ b/interim-server/src/routes/relations_single/portal_settings_handler.rs @@ -4,8 +4,11 @@ use axum::{ extract::{Path, State}, response::{Html, IntoResponse}, }; -use interim_models::{portal::Portal, workspace::Workspace}; -use interim_pgtypes::pg_class::PgClass; +use interim_models::{ + accessors::{Accessor, Actor, portal::PortalAccessor}, + portal::Portal, + workspace::Workspace, +}; use serde::Deserialize; use sqlx::postgres::types::Oid; use uuid::Uuid; @@ -41,30 +44,30 @@ pub(super) async fn get( navigator: Navigator, State(mut pooler): State, ) -> Result { - // FIXME: Check workspace authorization. - // FIXME ensure workspace corresponds to rel/portal, and that user has - // permission to access/alter both as needed. + let mut workspace_client = pooler + .acquire_for(workspace_id, RoleAssignment::User(user.id)) + .await?; + + let portal = PortalAccessor::new() + .id(portal_id) + .as_actor(Actor::User(user.id)) + .verify_workspace_id(workspace_id) + .verify_rel_oid(Oid(rel_oid)) + .verify_rel_ownership() + .using_app_db(&mut app_db) + .using_workspace_client(&mut workspace_client) + .fetch_one() + .await?; let workspace = Workspace::with_id(workspace_id) .fetch_one(&mut app_db) .await?; - let mut workspace_client = pooler - .acquire_for(workspace.id, RoleAssignment::User(user.id)) - .await?; - - let rel = PgClass::with_oid(Oid(rel_oid)) - .fetch_one(&mut workspace_client) - .await?; - - let portal = Portal::with_id(portal_id).fetch_one(&mut app_db).await?; - #[derive(Debug, Template)] #[template(path = "relations_single/portal_settings.html")] struct ResponseTemplate { navigator: Navigator, portal: Portal, - rel: PgClass, settings: Settings, workspace_nav: WorkspaceNav, } @@ -79,7 +82,6 @@ pub(super) async fn get( .build()?, navigator, portal, - rel, settings, } .render()?, diff --git a/interim-server/src/routes/relations_single/remove_field_handler.rs b/interim-server/src/routes/relations_single/remove_field_handler.rs index f2117c1..1317e1a 100644 --- a/interim-server/src/routes/relations_single/remove_field_handler.rs +++ b/interim-server/src/routes/relations_single/remove_field_handler.rs @@ -3,7 +3,10 @@ use axum::{ extract::{Path, State}, response::Response, }; -use interim_models::{field::Field, portal::Portal}; +use interim_models::{ + accessors::{Accessor, Actor, portal::PortalAccessor}, + field::Field, +}; use interim_pgtypes::{escape_identifier, pg_class::PgClass}; use serde::Deserialize; use sqlx::{postgres::types::Oid, query}; @@ -56,15 +59,34 @@ pub(super) async fn post( ) -> Result { // FIXME CSRF - // FIXME ensure workspace corresponds to rel/portal, and that user has - // permission to access/alter both as needed. - // Ensure field exists and belongs to portal. + // TODO: This leaks information if it fails. Instead, return + // `Err(AccessError::NotFound)` if not found. let field = Field::belonging_to_portal(portal_id) .with_id(field_id) .fetch_one(&mut app_db) .await?; + let mut workspace_client = pooler + .acquire_for(workspace_id, RoleAssignment::User(user.id)) + .await?; + let rel = PgClass::with_oid(Oid(rel_oid)) + .fetch_one(&mut workspace_client) + .await?; + + // For authorization only. + let _portal = PortalAccessor::new() + .id(portal_id) + .as_actor(Actor::User(user.id)) + .verify_workspace_id(workspace_id) + .verify_rel_oid(Oid(rel_oid)) + .verify_rel_ownership() + .using_rel(&rel) + .using_app_db(&mut app_db) + .using_workspace_client(&mut workspace_client) + .fetch_one() + .await?; + if delete_data == "true" && field.name.starts_with('_') { return Err(bad_request!("cannot delete data for a system column")); } @@ -72,13 +94,6 @@ pub(super) async fn post( field.delete(&mut app_db).await?; if delete_data == "true" { - let mut workspace_client = pooler - .acquire_for(workspace_id, RoleAssignment::User(user.id)) - .await?; - let portal = Portal::with_id(portal_id).fetch_one(&mut app_db).await?; - let rel = PgClass::with_oid(portal.class_oid) - .fetch_one(&mut workspace_client) - .await?; query(&format!( "alter table {ident} drop column if exists {col_esc}", ident = rel.get_identifier(), diff --git a/interim-server/src/routes/relations_single/set_filter_handler.rs b/interim-server/src/routes/relations_single/set_filter_handler.rs index 72506fb..743f827 100644 --- a/interim-server/src/routes/relations_single/set_filter_handler.rs +++ b/interim-server/src/routes/relations_single/set_filter_handler.rs @@ -1,8 +1,16 @@ -use axum::{debug_handler, extract::Path, response::Response}; +use axum::{ + debug_handler, + extract::{Path, State}, + response::Response, +}; // [`axum_extra`]'s form extractor is preferred: // https://docs.rs/axum-extra/0.10.1/axum_extra/extract/struct.Form.html#differences-from-axumextractform use axum_extra::extract::Form; -use interim_models::{expression::PgExpressionAny, portal::Portal}; +use interim_models::{ + accessors::{Accessor, Actor, portal::PortalAccessor}, + expression::PgExpressionAny, + portal::Portal, +}; use serde::Deserialize; use sqlx::postgres::types::Oid; use uuid::Uuid; @@ -12,6 +20,7 @@ use crate::{ errors::AppError, navigator::{Navigator, NavigatorPage as _}, user::CurrentUser, + workspace_pooler::{RoleAssignment, WorkspacePooler}, }; #[derive(Debug, Deserialize)] @@ -34,7 +43,8 @@ pub(super) struct FormBody { #[debug_handler(state = App)] pub(super) async fn post( AppDbConn(mut app_db): AppDbConn, - CurrentUser(_user): CurrentUser, + State(mut pooler): State, + CurrentUser(user): CurrentUser, navigator: Navigator, Path(PathParams { portal_id, @@ -43,13 +53,26 @@ pub(super) async fn post( }): Path, Form(form): Form, ) -> Result { - // FIXME: Check workspace authorization. - // FIXME ensure workspace corresponds to rel/portal, and that user has - // permission to access/alter both as needed. + // FIXME: csrf + + let mut workspace_client = pooler + .acquire_for(workspace_id, RoleAssignment::User(user.id)) + .await?; + + let portal = PortalAccessor::new() + .id(portal_id) + .as_actor(Actor::User(user.id)) + .verify_workspace_id(workspace_id) + .verify_rel_oid(Oid(rel_oid)) + .verify_rel_ownership() + .using_app_db(&mut app_db) + .using_workspace_client(&mut workspace_client) + .fetch_one() + .await?; - let portal = Portal::with_id(portal_id).fetch_one(&mut app_db).await?; let filter: Option = serde_json::from_str(&form.filter_expression.unwrap_or("null".to_owned()))?; + Portal::update() .id(portal.id) .table_filter(filter) @@ -61,7 +84,7 @@ pub(super) async fn post( .portal_page() .workspace_id(workspace_id) .rel_oid(Oid(rel_oid)) - .portal_id(portal_id) + .portal_id(portal.id) .build()? .redirect_to()) } diff --git a/interim-server/src/routes/relations_single/update_portal_name_handler.rs b/interim-server/src/routes/relations_single/update_portal_name_handler.rs index 3f08bf3..6b0a42d 100644 --- a/interim-server/src/routes/relations_single/update_portal_name_handler.rs +++ b/interim-server/src/routes/relations_single/update_portal_name_handler.rs @@ -3,8 +3,10 @@ use axum::{ extract::{Path, State}, response::Response, }; -use interim_models::portal::{Portal, RE_PORTAL_NAME}; -use interim_pgtypes::pg_class::PgClass; +use interim_models::{ + accessors::{Accessor, Actor, portal::PortalAccessor}, + portal::{Portal, RE_PORTAL_NAME}, +}; use serde::Deserialize; use sqlx::postgres::types::Oid; use uuid::Uuid; @@ -46,8 +48,6 @@ pub(super) async fn post( }): Path, ValidatedForm(FormBody { name }): ValidatedForm, ) -> Result { - // FIXME: Check workspace authorization. - let mut workspace_client = pooler .acquire_for( workspace_id, @@ -55,15 +55,19 @@ pub(super) async fn post( ) .await?; - let rel = PgClass::with_oid(Oid(rel_oid)) - .fetch_one(&mut workspace_client) + let portal = PortalAccessor::new() + .id(portal_id) + .as_actor(Actor::User(user.id)) + .verify_workspace_id(workspace_id) + .verify_rel_oid(Oid(rel_oid)) + .verify_rel_ownership() + .using_app_db(&mut app_db) + .using_workspace_client(&mut workspace_client) + .fetch_one() .await?; - // FIXME ensure that user has ownership of the table. - - let portal = Portal::with_id(portal_id).fetch_one(&mut app_db).await?; Portal::update() - .id(portal_id) + .id(portal.id) .name(name) .build()? .execute(&mut app_db) @@ -73,7 +77,7 @@ pub(super) async fn post( .portal_page() .workspace_id(workspace_id) .rel_oid(Oid(rel_oid)) - .portal_id(portal_id) + .portal_id(portal.id) .suffix("settings/") .build()? .redirect_to()) diff --git a/interim-server/src/routes/relations_single/update_prompts_handler.rs b/interim-server/src/routes/relations_single/update_prompts_handler.rs index dda0660..9e86282 100644 --- a/interim-server/src/routes/relations_single/update_prompts_handler.rs +++ b/interim-server/src/routes/relations_single/update_prompts_handler.rs @@ -9,7 +9,10 @@ use axum::{ // https://docs.rs/axum-extra/0.10.1/axum_extra/extract/struct.Form.html#differences-from-axumextractform use axum_extra::extract::Form; use interim_models::{ - field_form_prompt::FieldFormPrompt, language::Language, portal::Portal, workspace::Workspace, + accessors::{Accessor, Actor, portal::PortalAccessor}, + field::Field, + field_form_prompt::FieldFormPrompt, + language::Language, }; use serde::Deserialize; use sqlx::postgres::types::Oid; @@ -49,28 +52,39 @@ pub(super) async fn post( }): Path, Form(form): Form>, ) -> Result { - // FIXME: Check workspace authorization. - // FIXME ensure workspace corresponds to rel/portal, and that user has - // permission to access/alter both as needed. // FIXME CSRF - let portal = Portal::with_id(portal_id).fetch_one(&mut app_db).await?; - let workspace = Workspace::with_id(portal.workspace_id) - .fetch_one(&mut app_db) - .await?; - let mut workspace_client = workspace_pooler - .acquire_for(workspace.id, RoleAssignment::User(user.id)) + .acquire_for(workspace_id, RoleAssignment::User(user.id)) .await?; - // FIXME assert that fields all belong to the authorized portal + let portal = PortalAccessor::new() + .id(portal_id) + .as_actor(Actor::User(user.id)) + .verify_workspace_id(workspace_id) + .verify_rel_oid(Oid(rel_oid)) + .verify_rel_ownership() + .using_app_db(&mut app_db) + .using_workspace_client(&mut workspace_client) + .fetch_one() + .await?; + // TODO: This can be sped up somewhat with streams, because queries using + // `app_db` can run at the same time as others using `app_db` or + // `workspace_client`. for (name, content) in form { let mut name_split = name.split('.'); let field_id = name_split .next() .and_then(|value| Uuid::parse_str(value).ok()) .ok_or(bad_request!("expected input name to start with "))?; + + // For authorization. + let _field = Field::belonging_to_portal(portal.id) + .with_id(field_id) + .fetch_one(&mut app_db) + .await?; + let language = name_split .next() .and_then(|value| Language::from_str(value).ok()) @@ -95,6 +109,7 @@ pub(super) async fn post( .workspace_id(workspace_id) .rel_oid(Oid(rel_oid)) .portal_id(portal_id) + .suffix("form/") .build()? .redirect_to()) } diff --git a/interim-server/src/routes/relations_single/update_values_handler.rs b/interim-server/src/routes/relations_single/update_values_handler.rs index 656169d..8dd6bf6 100644 --- a/interim-server/src/routes/relations_single/update_values_handler.rs +++ b/interim-server/src/routes/relations_single/update_values_handler.rs @@ -5,8 +5,13 @@ use axum::{ extract::{Path, State}, response::{IntoResponse as _, Response}, }; -use interim_models::{datum::Datum, portal::Portal, workspace::Workspace}; -use interim_pgtypes::{escape_identifier, pg_attribute::PgAttribute, pg_class::PgClass}; +use interim_models::{ + accessors::{Accessor, Actor, portal::PortalAccessor}, + datum::Datum, +}; +use interim_pgtypes::{ + escape_identifier, pg_acl::PgPrivilegeType, pg_attribute::PgAttribute, pg_class::PgClass, +}; use serde::Deserialize; use serde_json::json; use sqlx::{Acquire as _, postgres::types::Oid, query}; @@ -54,28 +59,32 @@ pub(super) async fn post( }): Path, Json(form): Json, ) -> Result { - // FIXME: Check workspace authorization. - // FIXME ensure workspace corresponds to rel/portal, and that user has - // permission to access/alter both as needed. - // Prevent users from modifying Phonograph metadata columns. if form.cells.iter().any(|cell| cell.column.starts_with('_')) { return Err(forbidden!("access denied to update system metadata column")); } - let portal = Portal::with_id(portal_id).fetch_one(&mut app_db).await?; - let workspace = Workspace::with_id(portal.workspace_id) - .fetch_one(&mut app_db) - .await?; - let mut workspace_client = workspace_pooler - .acquire_for(workspace.id, RoleAssignment::User(user.id)) + .acquire_for(workspace_id, RoleAssignment::User(user.id)) .await?; - let rel = PgClass::with_oid(portal.class_oid) + let rel = PgClass::with_oid(Oid(rel_oid)) .fetch_one(&mut workspace_client) .await?; + // For authorization only. + let _portal = PortalAccessor::new() + .id(portal_id) + .as_actor(Actor::User(user.id)) + .verify_workspace_id(workspace_id) + .verify_rel_oid(Oid(rel_oid)) + .verify_rel_permissions([PgPrivilegeType::Update]) + .using_rel(&rel) + .using_app_db(&mut app_db) + .using_workspace_client(&mut workspace_client) + .fetch_one() + .await?; + let pkey_attrs = PgAttribute::pkeys_for_rel(Oid(rel_oid)) .fetch_all(&mut workspace_client) .await?; diff --git a/interim-server/src/routes/workspaces_multi/add_handlers.rs b/interim-server/src/routes/workspaces_multi/add_handlers.rs index 5d9e83d..dcc02e3 100644 --- a/interim-server/src/routes/workspaces_multi/add_handlers.rs +++ b/interim-server/src/routes/workspaces_multi/add_handlers.rs @@ -3,15 +3,13 @@ use interim_models::{ client::AppDbClient, cluster::Cluster, user::User, workspace::Workspace, workspace_user_perm::WorkspaceMembership, }; -use interim_pgtypes::{client::WorkspaceClient, escape_identifier}; +use interim_pgtypes::{client::WorkspaceClient, escape_identifier, rolnames::ROLE_PREFIX_USER}; use sqlx::{Connection as _, PgConnection, query}; use crate::{ app::AppDbConn, errors::AppError, navigator::{Navigator, NavigatorPage as _}, - roles::ROLE_PREFIX_USER, - settings::Settings, user::CurrentUser, workspace_pooler::{RoleAssignment, WorkspacePooler}, workspace_utils::PHONO_TABLE_NAMESPACE, @@ -22,7 +20,6 @@ use crate::{ /// 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, diff --git a/interim-server/src/routes/workspaces_single/add_service_credential_handler.rs b/interim-server/src/routes/workspaces_single/add_service_credential_handler.rs index 9b41577..3653f06 100644 --- a/interim-server/src/routes/workspaces_single/add_service_credential_handler.rs +++ b/interim-server/src/routes/workspaces_single/add_service_credential_handler.rs @@ -4,7 +4,10 @@ use axum::{ response::IntoResponse, }; use interim_models::{service_cred::ServiceCred, workspace::Workspace}; -use interim_pgtypes::escape_identifier; +use interim_pgtypes::{ + escape_identifier, + rolnames::{ROLE_PREFIX_SERVICE_CRED, SERVICE_CRED_CONN_LIMIT, SERVICE_CRED_SUFFIX_LEN}, +}; use rand::distributions::{Alphanumeric, DistString}; use redact::Secret; use serde::Deserialize; @@ -15,7 +18,6 @@ use crate::{ app::{App, AppDbConn}, errors::AppError, navigator::{Navigator, NavigatorPage}, - roles::{ROLE_PREFIX_SERVICE_CRED, SERVICE_CRED_CONN_LIMIT, SERVICE_CRED_SUFFIX_LEN}, user::CurrentUser, workspace_pooler::{RoleAssignment, WorkspacePooler}, workspace_utils::PHONO_TABLE_NAMESPACE, diff --git a/interim-server/src/routes/workspaces_single/add_table_handler.rs b/interim-server/src/routes/workspaces_single/add_table_handler.rs index 4369af3..65ef54e 100644 --- a/interim-server/src/routes/workspaces_single/add_table_handler.rs +++ b/interim-server/src/routes/workspaces_single/add_table_handler.rs @@ -2,7 +2,13 @@ use axum::{ extract::{Path, State}, response::IntoResponse, }; -use interim_pgtypes::escape_identifier; +use interim_pgtypes::{ + escape_identifier, + rolnames::{ + ROLE_PREFIX_TABLE_OWNER, ROLE_PREFIX_TABLE_READER, ROLE_PREFIX_TABLE_WRITER, + ROLE_PREFIX_USER, + }, +}; use serde::Deserialize; use sqlx::query; use uuid::Uuid; @@ -10,10 +16,6 @@ use uuid::Uuid; use crate::{ errors::AppError, navigator::{Navigator, NavigatorPage as _}, - roles::{ - ROLE_PREFIX_TABLE_OWNER, ROLE_PREFIX_TABLE_READER, ROLE_PREFIX_TABLE_WRITER, - ROLE_PREFIX_USER, - }, user::CurrentUser, workspace_pooler::{RoleAssignment, WorkspacePooler}, workspace_utils::PHONO_TABLE_NAMESPACE, diff --git a/interim-server/src/workspace_pooler.rs b/interim-server/src/workspace_pooler.rs index ff131e4..8d84df7 100644 --- a/interim-server/src/workspace_pooler.rs +++ b/interim-server/src/workspace_pooler.rs @@ -4,12 +4,12 @@ use anyhow::Result; use axum::extract::FromRef; use derive_builder::Builder; use interim_models::{client::AppDbClient, workspace::Workspace}; -use interim_pgtypes::client::WorkspaceClient; +use interim_pgtypes::{client::WorkspaceClient, rolnames::ROLE_PREFIX_USER}; use sqlx::{Executor, PgPool, postgres::PgPoolOptions, raw_sql}; use tokio::sync::{OnceCell, RwLock}; use uuid::Uuid; -use crate::{app::App, roles::ROLE_PREFIX_USER}; +use crate::app::App; const MAX_CONNECTIONS: u32 = 4; const IDLE_SECONDS: u64 = 3600; @@ -109,15 +109,15 @@ discard sequences; pub async fn close_for(&mut self, base_id: Uuid) -> Result<()> { let pools = self.pools.read().await; - if let Some(cell) = pools.get(&base_id) { - if let Some(pool) = cell.get() { - let pool = pool.clone(); - drop(pools); // Release read lock - let mut pools = self.pools.write().await; - pools.remove(&base_id); - drop(pools); // Release write lock - pool.close().await; - } + if let Some(cell) = pools.get(&base_id) + && let Some(pool) = cell.get() + { + let pool = pool.clone(); + drop(pools); // Release read lock + let mut pools = self.pools.write().await; + pools.remove(&base_id); + drop(pools); // Release write lock + pool.close().await; } Ok(()) }