implement accessor pattern for authorized portal access
This commit is contained in:
parent
7f63cbb521
commit
5d5acb09bb
30 changed files with 601 additions and 190 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -1757,6 +1757,7 @@ dependencies = [
|
||||||
"bigdecimal",
|
"bigdecimal",
|
||||||
"chrono",
|
"chrono",
|
||||||
"derive_builder",
|
"derive_builder",
|
||||||
|
"futures",
|
||||||
"interim-pgtypes",
|
"interim-pgtypes",
|
||||||
"redact",
|
"redact",
|
||||||
"regex",
|
"regex",
|
||||||
|
|
@ -1765,6 +1766,7 @@ dependencies = [
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"strum",
|
"strum",
|
||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
"validator",
|
"validator",
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ version.workspace = true
|
||||||
bigdecimal = { workspace = true }
|
bigdecimal = { workspace = true }
|
||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
derive_builder = { workspace = true }
|
derive_builder = { workspace = true }
|
||||||
|
futures = { workspace = true }
|
||||||
interim-pgtypes = { path = "../interim-pgtypes" }
|
interim-pgtypes = { path = "../interim-pgtypes" }
|
||||||
redact = { workspace = true }
|
redact = { workspace = true }
|
||||||
regex = { workspace = true }
|
regex = { workspace = true }
|
||||||
|
|
@ -15,6 +16,7 @@ serde_json = { workspace = true }
|
||||||
sqlx = { workspace = true }
|
sqlx = { workspace = true }
|
||||||
strum = { workspace = true }
|
strum = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
|
tracing = { workspace = true }
|
||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
uuid = { workspace = true }
|
uuid = { workspace = true }
|
||||||
validator = { workspace = true }
|
validator = { workspace = true }
|
||||||
|
|
|
||||||
53
interim-models/src/accessors/mod.rs
Normal file
53
interim-models/src/accessors/mod.rs
Normal file
|
|
@ -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<sqlx::Error>),
|
||||||
|
|
||||||
|
#[error("not found or access denied")]
|
||||||
|
NotFound,
|
||||||
|
|
||||||
|
#[error("incomplete access spec: {0}")]
|
||||||
|
Incomplete(UninitializedFieldError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<sqlx::Error> for AccessError {
|
||||||
|
fn from(value: sqlx::Error) -> Self {
|
||||||
|
Self::Database(Arc::new(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<UninitializedFieldError> 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<T>: 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<Output = Result<T, AccessError>>;
|
||||||
|
}
|
||||||
202
interim-models/src/accessors/portal.rs
Normal file
202
interim-models/src/accessors/portal.rs
Normal file
|
|
@ -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<Uuid>,
|
||||||
|
|
||||||
|
/// Optionally verify that the portal is associated with a specific relation
|
||||||
|
/// OID.
|
||||||
|
#[builder(default, setter(strip_option))]
|
||||||
|
verify_rel_oid: Option<Oid>,
|
||||||
|
|
||||||
|
/// Optionally verify that the actor has or inherits specific Postgres
|
||||||
|
/// permissions on the relation in the backing database.
|
||||||
|
#[builder(default, setter(into))]
|
||||||
|
// Using [`HashSet<PgPrivilegeType>`] 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<PgPrivilegeType>,
|
||||||
|
|
||||||
|
// 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<Portal> for PortalAccessor<'a> {
|
||||||
|
async fn fetch_one(self) -> Result<Portal, AccessError> {
|
||||||
|
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<PgPrivilegeType> = 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
#![feature(bool_to_result)] // Enable support for `ok_or()` on bools.
|
||||||
|
|
||||||
|
pub mod accessors;
|
||||||
pub mod client;
|
pub mod client;
|
||||||
pub mod cluster;
|
pub mod cluster;
|
||||||
pub mod datum;
|
pub mod datum;
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,9 @@ use crate::escape_identifier;
|
||||||
|
|
||||||
/// Newtype to differentiate between workspace and application database
|
/// Newtype to differentiate between workspace and application database
|
||||||
/// connections.
|
/// connections.
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct WorkspaceClient {
|
pub struct WorkspaceClient {
|
||||||
pub(crate) conn: PoolConnection<Postgres>,
|
conn: PoolConnection<Postgres>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WorkspaceClient {
|
impl WorkspaceClient {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ pub mod pg_class;
|
||||||
pub mod pg_database;
|
pub mod pg_database;
|
||||||
pub mod pg_namespace;
|
pub mod pg_namespace;
|
||||||
pub mod pg_role;
|
pub mod pg_role;
|
||||||
|
pub mod rolnames;
|
||||||
|
|
||||||
/// Given a raw identifier (such as a table name, column name, etc.), format it
|
/// 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.
|
/// so that it may be safely interpolated into a SQL query.
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ where attrelid = $1 and attnum > 0 and not attisdropped
|
||||||
"#,
|
"#,
|
||||||
&self.rel_oid
|
&self.rel_oid
|
||||||
)
|
)
|
||||||
.fetch_all(&mut *client.conn)
|
.fetch_all(client.get_conn())
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -131,7 +131,7 @@ where i.indrelid = $1 and i.indisprimary;
|
||||||
"#,
|
"#,
|
||||||
&self.rel_oid
|
&self.rel_oid
|
||||||
)
|
)
|
||||||
.fetch_all(&mut *client.conn)
|
.fetch_all(client.get_conn())
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ impl PgClass {
|
||||||
&self,
|
&self,
|
||||||
client: &mut WorkspaceClient,
|
client: &mut WorkspaceClient,
|
||||||
) -> Result<PgNamespace, sqlx::Error> {
|
) -> Result<PgNamespace, sqlx::Error> {
|
||||||
PgNamespace::fetch_by_oid(self.relnamespace, &mut *client.conn)
|
PgNamespace::fetch_by_oid(self.relnamespace, client.get_conn())
|
||||||
.await?
|
.await?
|
||||||
// If client has access to the class, it would expect to have access
|
// 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.
|
// to the namespace that contains it. If not, that's an error.
|
||||||
|
|
@ -178,7 +178,7 @@ where
|
||||||
impl WithOidQuery {
|
impl WithOidQuery {
|
||||||
pub async fn fetch_one(self, client: &mut WorkspaceClient) -> Result<PgClass, sqlx::Error> {
|
pub async fn fetch_one(self, client: &mut WorkspaceClient) -> Result<PgClass, sqlx::Error> {
|
||||||
with_oid_sqlx_query!(self.oid)
|
with_oid_sqlx_query!(self.oid)
|
||||||
.fetch_one(&mut *client.conn)
|
.fetch_one(client.get_conn())
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -187,7 +187,7 @@ impl WithOidQuery {
|
||||||
client: &mut WorkspaceClient,
|
client: &mut WorkspaceClient,
|
||||||
) -> Result<Option<PgClass>, sqlx::Error> {
|
) -> Result<Option<PgClass>, sqlx::Error> {
|
||||||
with_oid_sqlx_query!(self.oid)
|
with_oid_sqlx_query!(self.oid)
|
||||||
.fetch_optional(&mut *client.conn)
|
.fetch_optional(client.get_conn())
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -235,7 +235,7 @@ where
|
||||||
"#,
|
"#,
|
||||||
kinds_i8.as_slice(),
|
kinds_i8.as_slice(),
|
||||||
)
|
)
|
||||||
.fetch_all(&mut *client.conn)
|
.fetch_all(client.get_conn())
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ from pg_database
|
||||||
where datname = current_database()
|
where datname = current_database()
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.fetch_one(&mut *client.conn)
|
.fetch_one(client.get_conn())
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,24 +9,34 @@ use crate::client::WorkspaceClient;
|
||||||
pub struct PgRole {
|
pub struct PgRole {
|
||||||
/// ID of role
|
/// ID of role
|
||||||
pub oid: Oid,
|
pub oid: Oid,
|
||||||
|
|
||||||
/// Role name
|
/// Role name
|
||||||
pub rolname: String,
|
pub rolname: String,
|
||||||
|
|
||||||
/// Role has superuser privileges
|
/// Role has superuser privileges
|
||||||
pub rolsuper: bool,
|
pub rolsuper: bool,
|
||||||
|
|
||||||
/// Role automatically inherits privileges of roles it is a member of
|
/// Role automatically inherits privileges of roles it is a member of
|
||||||
pub rolinherit: bool,
|
pub rolinherit: bool,
|
||||||
|
|
||||||
/// Role can create more roles
|
/// Role can create more roles
|
||||||
pub rolcreaterole: bool,
|
pub rolcreaterole: bool,
|
||||||
|
|
||||||
/// Role can create databases
|
/// Role can create databases
|
||||||
pub rolcreatedb: bool,
|
pub rolcreatedb: bool,
|
||||||
|
|
||||||
/// Role can log in. That is, this role can be given as the initial session authorization identifier
|
/// Role can log in. That is, this role can be given as the initial session authorization identifier
|
||||||
pub rolcanlogin: bool,
|
pub rolcanlogin: bool,
|
||||||
|
|
||||||
/// Role is a replication role. A replication role can initiate replication connections and create and drop replication slots.
|
/// Role is a replication role. A replication role can initiate replication connections and create and drop replication slots.
|
||||||
pub rolreplication: bool,
|
pub rolreplication: bool,
|
||||||
|
|
||||||
/// For roles that can log in, this sets maximum number of concurrent connections this role can make. -1 means no limit.
|
/// For roles that can log in, this sets maximum number of concurrent connections this role can make. -1 means no limit.
|
||||||
pub rolconnlimit: i32,
|
pub rolconnlimit: i32,
|
||||||
|
|
||||||
/// Password expiry time (only used for password authentication); null if no expiration
|
/// Password expiry time (only used for password authentication); null if no expiration
|
||||||
pub rolvaliduntil: Option<DateTime<Utc>>,
|
pub rolvaliduntil: Option<DateTime<Utc>>,
|
||||||
|
|
||||||
/// Role bypasses every row-level security policy, see Section 5.9 for more information.
|
/// Role bypasses every row-level security policy, see Section 5.9 for more information.
|
||||||
pub rolbypassrls: bool,
|
pub rolbypassrls: bool,
|
||||||
}
|
}
|
||||||
|
|
@ -71,7 +81,7 @@ where rolname = any($1)
|
||||||
"#,
|
"#,
|
||||||
self.names.as_slice()
|
self.names.as_slice()
|
||||||
)
|
)
|
||||||
.fetch_all(&mut *client.conn)
|
.fetch_all(client.get_conn())
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -106,11 +116,16 @@ where starts_with(rolname, $1)
|
||||||
"#,
|
"#,
|
||||||
self.prefix
|
self.prefix
|
||||||
)
|
)
|
||||||
.fetch_all(&mut *client.conn)
|
.fetch_all(client.get_conn())
|
||||||
.await
|
.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)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct RoleTree {
|
pub struct RoleTree {
|
||||||
pub role: PgRole,
|
pub role: PgRole,
|
||||||
|
|
@ -187,7 +202,7 @@ impl MembersOfOidQuery {
|
||||||
",
|
",
|
||||||
)
|
)
|
||||||
.bind(self.role)
|
.bind(self.role)
|
||||||
.fetch_all(&mut *client.conn)
|
.fetch_all(client.get_conn())
|
||||||
.await?;
|
.await?;
|
||||||
Ok(rows
|
Ok(rows
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -231,7 +246,7 @@ impl MembersOfRolnameQuery {
|
||||||
",
|
",
|
||||||
)
|
)
|
||||||
.bind(self.role.as_str() as &str)
|
.bind(self.role.as_str() as &str)
|
||||||
.fetch_all(&mut *client.conn)
|
.fetch_all(client.get_conn())
|
||||||
.await?;
|
.await?;
|
||||||
Ok(rows
|
Ok(rows
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -273,7 +288,7 @@ from (
|
||||||
",
|
",
|
||||||
)
|
)
|
||||||
.bind(self.role_oid)
|
.bind(self.role_oid)
|
||||||
.fetch_all(&mut *client.conn)
|
.fetch_all(client.get_conn())
|
||||||
.await?;
|
.await?;
|
||||||
Ok(rows
|
Ok(rows
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -315,7 +330,7 @@ from (
|
||||||
",
|
",
|
||||||
)
|
)
|
||||||
.bind(self.rolname)
|
.bind(self.rolname)
|
||||||
.fetch_all(&mut *client.conn)
|
.fetch_all(client.get_conn())
|
||||||
.await?;
|
.await?;
|
||||||
Ok(rows
|
Ok(rows
|
||||||
.iter()
|
.iter()
|
||||||
|
|
|
||||||
7
interim-pgtypes/src/rolnames.rs
Normal file
7
interim-pgtypes/src/rolnames.rs
Normal file
|
|
@ -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;
|
||||||
|
|
@ -5,31 +5,31 @@ use axum::response::{IntoResponse, Response};
|
||||||
|
|
||||||
macro_rules! forbidden {
|
macro_rules! forbidden {
|
||||||
($message:literal) => {
|
($message:literal) => {
|
||||||
AppError::Forbidden($message.to_owned())
|
crate::errors::AppError::Forbidden($message.to_owned())
|
||||||
};
|
};
|
||||||
|
|
||||||
($message:literal, $($param:expr),+) => {
|
($message:literal, $($param:expr),+) => {
|
||||||
AppError::Forbidden(format!($message, $($param)+))
|
crate::errors::AppError::Forbidden(format!($message, $($param)+))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! not_found {
|
macro_rules! not_found {
|
||||||
($message:literal) => {
|
($message:literal) => {
|
||||||
AppError::NotFound($message.to_owned())
|
crate::errors::AppError::NotFound($message.to_owned())
|
||||||
};
|
};
|
||||||
|
|
||||||
($message:literal, $($param:expr),+) => {
|
($message:literal, $($param:expr),+) => {
|
||||||
AppError::NotFound(format!($message, $($param)+))
|
crate::errors::AppError::NotFound(format!($message, $($param)+))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! bad_request {
|
macro_rules! bad_request {
|
||||||
($message:literal) => {
|
($message:literal) => {
|
||||||
AppError::BadRequest(format!($message))
|
crate::errors::AppError::BadRequest(format!($message))
|
||||||
};
|
};
|
||||||
|
|
||||||
($message:literal, $($param:expr),+) => {
|
($message:literal, $($param:expr),+) => {
|
||||||
AppError::BadRequest(format!($message, $($param)+))
|
crate::errors::AppError::BadRequest(format!($message, $($param)+))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,20 +6,13 @@ use interim_pgtypes::{
|
||||||
client::WorkspaceClient,
|
client::WorkspaceClient,
|
||||||
pg_acl::{PgAclItem, PgPrivilegeType},
|
pg_acl::{PgAclItem, PgPrivilegeType},
|
||||||
pg_class::PgClass,
|
pg_class::PgClass,
|
||||||
|
rolnames::{ROLE_PREFIX_TABLE_OWNER, ROLE_PREFIX_TABLE_READER, ROLE_PREFIX_TABLE_WRITER},
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::{postgres::types::Oid, prelude::FromRow, query_as};
|
use sqlx::{postgres::types::Oid, prelude::FromRow, query_as};
|
||||||
|
|
||||||
use crate::errors::AppError;
|
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: custom error type
|
||||||
// TODO: make params and result references
|
// TODO: make params and result references
|
||||||
fn get_table_role(
|
fn get_table_role(
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ use interim_models::{
|
||||||
portal::Portal,
|
portal::Portal,
|
||||||
presentation::{Presentation, TextInputMode},
|
presentation::{Presentation, TextInputMode},
|
||||||
};
|
};
|
||||||
use interim_pgtypes::{pg_attribute::PgAttribute, pg_class::PgClass};
|
use interim_pgtypes::pg_attribute::PgAttribute;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
|
@ -29,19 +29,22 @@ pub(super) struct PathParams {
|
||||||
portal_id: Uuid,
|
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(
|
pub(super) async fn get(
|
||||||
State(settings): State<Settings>,
|
State(settings): State<Settings>,
|
||||||
State(mut pooler): State<WorkspacePooler>,
|
State(mut pooler): State<WorkspacePooler>,
|
||||||
AppDbConn(mut app_db): AppDbConn,
|
AppDbConn(mut app_db): AppDbConn,
|
||||||
Path(PathParams { portal_id }): Path<PathParams>,
|
Path(PathParams { portal_id }): Path<PathParams>,
|
||||||
) -> Result<Response, AppError> {
|
) -> Result<Response, AppError> {
|
||||||
|
// 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)
|
let portal = Portal::with_id(portal_id)
|
||||||
.fetch_optional(&mut app_db)
|
.fetch_optional(&mut app_db)
|
||||||
.await?
|
.await?
|
||||||
.ok_or(not_found!("form not found"))?;
|
.ok_or(not_found!("form not found"))?;
|
||||||
|
|
||||||
// FIXME: auth
|
|
||||||
|
|
||||||
// WARNING: This client is connected with full workspace privileges. Even
|
// WARNING: This client is connected with full workspace privileges. Even
|
||||||
// more so than usual, the Phonograph server is responsible for ensuring all
|
// more so than usual, the Phonograph server is responsible for ensuring all
|
||||||
// auth checks are performed properly.
|
// auth checks are performed properly.
|
||||||
|
|
@ -52,9 +55,6 @@ pub(super) async fn get(
|
||||||
.acquire_for(portal.workspace_id, RoleAssignment::Root)
|
.acquire_for(portal.workspace_id, RoleAssignment::Root)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let rel = PgClass::with_oid(portal.class_oid)
|
|
||||||
.fetch_one(&mut workspace_client)
|
|
||||||
.await?;
|
|
||||||
let attrs: HashMap<String, PgAttribute> = PgAttribute::all_for_rel(portal.class_oid)
|
let attrs: HashMap<String, PgAttribute> = PgAttribute::all_for_rel(portal.class_oid)
|
||||||
.fetch_all(&mut workspace_client)
|
.fetch_all(&mut workspace_client)
|
||||||
.await?
|
.await?
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,9 @@ use axum::{
|
||||||
// https://docs.rs/axum-extra/0.10.1/axum_extra/extract/struct.Form.html#differences-from-axumextractform
|
// https://docs.rs/axum-extra/0.10.1/axum_extra/extract/struct.Form.html#differences-from-axumextractform
|
||||||
use axum_extra::extract::Form;
|
use axum_extra::extract::Form;
|
||||||
use interim_models::{
|
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 interim_pgtypes::{escape_identifier, pg_class::PgClass};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
@ -49,7 +51,7 @@ pub(super) struct FormBody {
|
||||||
/// [`PathParams`].
|
/// [`PathParams`].
|
||||||
#[debug_handler(state = App)]
|
#[debug_handler(state = App)]
|
||||||
pub(super) async fn post(
|
pub(super) async fn post(
|
||||||
State(mut workspace_pooler): State<WorkspacePooler>,
|
State(mut pooler): State<WorkspacePooler>,
|
||||||
AppDbConn(mut app_db): AppDbConn,
|
AppDbConn(mut app_db): AppDbConn,
|
||||||
CurrentUser(user): CurrentUser,
|
CurrentUser(user): CurrentUser,
|
||||||
navigator: Navigator,
|
navigator: Navigator,
|
||||||
|
|
@ -60,28 +62,33 @@ pub(super) async fn post(
|
||||||
}): Path<PathParams>,
|
}): Path<PathParams>,
|
||||||
Form(form): Form<FormBody>,
|
Form(form): Form<FormBody>,
|
||||||
) -> Result<Response, AppError> {
|
) -> Result<Response, AppError> {
|
||||||
// FIXME: Check workspace authorization.
|
// FIXME: csrf
|
||||||
// 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 mut workspace_client = pooler
|
||||||
let workspace = Workspace::with_id(portal.workspace_id)
|
.acquire_for(workspace_id, RoleAssignment::User(user.id))
|
||||||
.fetch_one(&mut app_db)
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut workspace_client = workspace_pooler
|
let rel = PgClass::with_oid(Oid(rel_oid))
|
||||||
.acquire_for(workspace.id, RoleAssignment::User(user.id))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let class = PgClass::with_oid(portal.class_oid)
|
|
||||||
.fetch_one(&mut workspace_client)
|
.fetch_one(&mut workspace_client)
|
||||||
.await?;
|
.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)?;
|
let presentation = Presentation::try_from(form.presentation_form)?;
|
||||||
|
|
||||||
query(&format!(
|
query(&format!(
|
||||||
"alter table {ident} add column if not exists {col} {typ}",
|
"alter table {ident} add column if not exists {col} {typ}",
|
||||||
ident = class.get_identifier(),
|
ident = rel.get_identifier(),
|
||||||
col = escape_identifier(&form.name),
|
col = escape_identifier(&form.name),
|
||||||
typ = presentation.attr_data_type_fragment(),
|
typ = presentation.attr_data_type_fragment(),
|
||||||
))
|
))
|
||||||
|
|
@ -90,8 +97,8 @@ pub(super) async fn post(
|
||||||
query(&format!(
|
query(&format!(
|
||||||
"grant insert ({col}), update ({col}) on table {ident} to {writer_role}",
|
"grant insert ({col}), update ({col}) on table {ident} to {writer_role}",
|
||||||
col = escape_identifier(&form.name),
|
col = escape_identifier(&form.name),
|
||||||
ident = class.get_identifier(),
|
ident = rel.get_identifier(),
|
||||||
writer_role = escape_identifier(&get_writer_role(&class)?),
|
writer_role = escape_identifier(&get_writer_role(&rel)?),
|
||||||
))
|
))
|
||||||
.execute(workspace_client.get_conn())
|
.execute(workspace_client.get_conn())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,13 @@ use axum::{
|
||||||
response::{Html, IntoResponse},
|
response::{Html, IntoResponse},
|
||||||
};
|
};
|
||||||
use interim_models::{
|
use interim_models::{
|
||||||
field::Field, field_form_prompt::FieldFormPrompt, form_transition::FormTransition,
|
accessors::{Accessor as _, Actor, portal::PortalAccessor},
|
||||||
language::Language, portal::Portal, workspace::Workspace,
|
field::Field,
|
||||||
|
field_form_prompt::FieldFormPrompt,
|
||||||
|
form_transition::FormTransition,
|
||||||
|
language::Language,
|
||||||
|
portal::Portal,
|
||||||
|
workspace::Workspace,
|
||||||
};
|
};
|
||||||
use interim_pgtypes::pg_attribute::PgAttribute;
|
use interim_pgtypes::pg_attribute::PgAttribute;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
@ -49,17 +54,23 @@ pub(super) async fn get(
|
||||||
navigator: Navigator,
|
navigator: Navigator,
|
||||||
State(mut pooler): State<WorkspacePooler>,
|
State(mut pooler): State<WorkspacePooler>,
|
||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
// FIXME: Check workspace authorization.
|
let mut workspace_client = pooler
|
||||||
// FIXME ensure workspace corresponds to rel/portal, and that user has
|
.acquire_for(workspace_id, RoleAssignment::User(user.id))
|
||||||
// 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)
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut workspace_client = pooler
|
let portal = PortalAccessor::new()
|
||||||
.acquire_for(workspace.id, RoleAssignment::User(user.id))
|
.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?;
|
.await?;
|
||||||
|
|
||||||
let attrs: HashMap<String, PgAttribute> = PgAttribute::all_for_rel(portal.class_oid)
|
let attrs: HashMap<String, PgAttribute> = PgAttribute::all_for_rel(portal.class_oid)
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,19 @@ use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
response::{IntoResponse as _, Response},
|
response::{IntoResponse as _, Response},
|
||||||
};
|
};
|
||||||
use interim_models::{datum::Datum, field::Field, portal::Portal};
|
use interim_models::{
|
||||||
use interim_pgtypes::{escape_identifier, pg_attribute::PgAttribute, pg_class::PgClass};
|
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 serde::{Deserialize, Serialize};
|
||||||
use sqlx::{postgres::PgRow, query};
|
use sqlx::{
|
||||||
|
postgres::{PgRow, types::Oid},
|
||||||
|
query,
|
||||||
|
};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -22,6 +31,8 @@ use crate::{
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
pub(super) struct PathParams {
|
pub(super) struct PathParams {
|
||||||
portal_id: Uuid,
|
portal_id: Uuid,
|
||||||
|
rel_oid: u32,
|
||||||
|
workspace_id: Uuid,
|
||||||
}
|
}
|
||||||
|
|
||||||
const FRONTEND_ROW_LIMIT: i64 = 1000;
|
const FRONTEND_ROW_LIMIT: i64 = 1000;
|
||||||
|
|
@ -33,19 +44,33 @@ const FRONTEND_ROW_LIMIT: i64 = 1000;
|
||||||
pub(super) async fn get(
|
pub(super) async fn get(
|
||||||
State(mut workspace_pooler): State<WorkspacePooler>,
|
State(mut workspace_pooler): State<WorkspacePooler>,
|
||||||
AppDbConn(mut app_db): AppDbConn,
|
AppDbConn(mut app_db): AppDbConn,
|
||||||
CurrentUser(current_user): CurrentUser,
|
CurrentUser(user): CurrentUser,
|
||||||
Path(PathParams { portal_id }): Path<PathParams>,
|
Path(PathParams {
|
||||||
|
portal_id,
|
||||||
|
rel_oid,
|
||||||
|
workspace_id,
|
||||||
|
}): Path<PathParams>,
|
||||||
) -> Result<Response, AppError> {
|
) -> Result<Response, AppError> {
|
||||||
// FIXME auth
|
|
||||||
let portal = Portal::with_id(portal_id).fetch_one(&mut app_db).await?;
|
|
||||||
|
|
||||||
let mut workspace_client = workspace_pooler
|
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?;
|
.await?;
|
||||||
let rel = PgClass::with_oid(portal.class_oid)
|
|
||||||
|
let rel = PgClass::with_oid(Oid(rel_oid))
|
||||||
.fetch_one(&mut workspace_client)
|
.fetch_one(&mut workspace_client)
|
||||||
.await?;
|
.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)
|
let attrs = PgAttribute::all_for_rel(portal.class_oid)
|
||||||
.fetch_all(&mut workspace_client)
|
.fetch_all(&mut workspace_client)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,11 @@ use axum::{
|
||||||
// [`axum_extra`]'s form extractor is required to support repeated keys:
|
// [`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
|
// https://docs.rs/axum-extra/0.10.1/axum_extra/extract/struct.Form.html#differences-from-axumextractform
|
||||||
use axum_extra::extract::Form;
|
use axum_extra::extract::Form;
|
||||||
use interim_models::{datum::Datum, portal::Portal, workspace::Workspace};
|
use interim_models::{
|
||||||
use interim_pgtypes::{escape_identifier, pg_class::PgClass};
|
accessors::{Accessor as _, Actor, portal::PortalAccessor},
|
||||||
|
datum::Datum,
|
||||||
|
};
|
||||||
|
use interim_pgtypes::{escape_identifier, pg_acl::PgPrivilegeType, pg_class::PgClass};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use sqlx::{postgres::types::Oid, query};
|
use sqlx::{postgres::types::Oid, query};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
@ -47,24 +50,29 @@ pub(super) async fn post(
|
||||||
}): Path<PathParams>,
|
}): Path<PathParams>,
|
||||||
Form(form): Form<HashMap<String, Vec<String>>>,
|
Form(form): Form<HashMap<String, Vec<String>>>,
|
||||||
) -> Result<Response, AppError> {
|
) -> Result<Response, AppError> {
|
||||||
// FIXME: Check workspace authorization.
|
|
||||||
// FIXME ensure workspace corresponds to rel/portal, and that user has
|
|
||||||
// permission to access/alter both as needed.
|
|
||||||
// FIXME CSRF
|
// 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
|
let mut workspace_client = workspace_pooler
|
||||||
.acquire_for(workspace.id, RoleAssignment::User(user.id))
|
.acquire_for(workspace_id, RoleAssignment::User(user.id))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let rel = PgClass::with_oid(Oid(rel_oid))
|
let rel = PgClass::with_oid(Oid(rel_oid))
|
||||||
.fetch_one(&mut workspace_client)
|
.fetch_one(&mut workspace_client)
|
||||||
.await?;
|
.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<String> = form.keys().cloned().collect();
|
let col_names: Vec<String> = form.keys().cloned().collect();
|
||||||
|
|
||||||
// Prevent users from modifying Phonograph metadata columns.
|
// Prevent users from modifying Phonograph metadata columns.
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,12 @@ use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
response::{Html, IntoResponse as _, Response},
|
response::{Html, IntoResponse as _, Response},
|
||||||
};
|
};
|
||||||
use interim_models::{expression::PgExpressionAny, portal::Portal, workspace::Workspace};
|
use interim_models::{
|
||||||
use interim_pgtypes::pg_attribute::PgAttribute;
|
accessors::{Accessor, Actor, portal::PortalAccessor},
|
||||||
|
expression::PgExpressionAny,
|
||||||
|
workspace::Workspace,
|
||||||
|
};
|
||||||
|
use interim_pgtypes::{pg_acl::PgPrivilegeType, pg_attribute::PgAttribute};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::postgres::types::Oid;
|
use sqlx::postgres::types::Oid;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
@ -19,7 +23,7 @@ use crate::{
|
||||||
workspace_pooler::{RoleAssignment, WorkspacePooler},
|
workspace_pooler::{RoleAssignment, WorkspacePooler},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub(super) struct PathParams {
|
pub(super) struct PathParams {
|
||||||
portal_id: Uuid,
|
portal_id: Uuid,
|
||||||
rel_oid: u32,
|
rel_oid: u32,
|
||||||
|
|
@ -32,25 +36,29 @@ pub(super) struct PathParams {
|
||||||
/// module.
|
/// module.
|
||||||
pub(super) async fn get(
|
pub(super) async fn get(
|
||||||
State(settings): State<Settings>,
|
State(settings): State<Settings>,
|
||||||
State(mut workspace_pooler): State<WorkspacePooler>,
|
State(mut pooler): State<WorkspacePooler>,
|
||||||
AppDbConn(mut app_db): AppDbConn,
|
AppDbConn(mut app_db): AppDbConn,
|
||||||
CurrentUser(current_user): CurrentUser,
|
CurrentUser(user): CurrentUser,
|
||||||
navigator: Navigator,
|
navigator: Navigator,
|
||||||
Path(PathParams {
|
Path(PathParams {
|
||||||
portal_id,
|
portal_id,
|
||||||
workspace_id,
|
|
||||||
rel_oid,
|
rel_oid,
|
||||||
|
workspace_id,
|
||||||
}): Path<PathParams>,
|
}): Path<PathParams>,
|
||||||
) -> Result<Response, AppError> {
|
) -> Result<Response, AppError> {
|
||||||
// FIXME auth
|
let mut workspace_client = pooler
|
||||||
|
.acquire_for(workspace_id, RoleAssignment::User(user.id))
|
||||||
let workspace = Workspace::with_id(workspace_id)
|
|
||||||
.fetch_one(&mut app_db)
|
|
||||||
.await?;
|
.await?;
|
||||||
let portal = Portal::with_id(portal_id).fetch_one(&mut app_db).await?;
|
|
||||||
|
|
||||||
let mut workspace_client = workspace_pooler
|
let portal = PortalAccessor::new()
|
||||||
.acquire_for(portal.workspace_id, RoleAssignment::User(current_user.id))
|
.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?;
|
.await?;
|
||||||
|
|
||||||
let attrs = PgAttribute::all_for_rel(portal.class_oid)
|
let attrs = PgAttribute::all_for_rel(portal.class_oid)
|
||||||
|
|
@ -70,6 +78,10 @@ pub(super) async fn get(
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
let workspace = Workspace::with_id(portal.workspace_id)
|
||||||
|
.fetch_one(&mut app_db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "portal_table.html")]
|
#[template(path = "portal_table.html")]
|
||||||
struct ResponseTemplate {
|
struct ResponseTemplate {
|
||||||
|
|
@ -86,11 +98,11 @@ pub(super) async fn get(
|
||||||
filter: portal.table_filter.0,
|
filter: portal.table_filter.0,
|
||||||
navbar: WorkspaceNav::builder()
|
navbar: WorkspaceNav::builder()
|
||||||
.navigator(navigator)
|
.navigator(navigator)
|
||||||
.workspace(workspace.clone())
|
.workspace(workspace)
|
||||||
.populate_rels(&mut app_db, &mut workspace_client)
|
.populate_rels(&mut app_db, &mut workspace_client)
|
||||||
.await?
|
.await?
|
||||||
.current(NavLocation::Rel(
|
.current(NavLocation::Rel(
|
||||||
Oid(rel_oid),
|
portal.class_oid,
|
||||||
Some(RelLocation::Portal(portal.id)),
|
Some(RelLocation::Portal(portal.id)),
|
||||||
))
|
))
|
||||||
.build()?,
|
.build()?,
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,11 @@ use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
response::{Html, IntoResponse},
|
response::{Html, IntoResponse},
|
||||||
};
|
};
|
||||||
use interim_models::{portal::Portal, workspace::Workspace};
|
use interim_models::{
|
||||||
use interim_pgtypes::pg_class::PgClass;
|
accessors::{Accessor, Actor, portal::PortalAccessor},
|
||||||
|
portal::Portal,
|
||||||
|
workspace::Workspace,
|
||||||
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use sqlx::postgres::types::Oid;
|
use sqlx::postgres::types::Oid;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
@ -41,30 +44,30 @@ pub(super) async fn get(
|
||||||
navigator: Navigator,
|
navigator: Navigator,
|
||||||
State(mut pooler): State<WorkspacePooler>,
|
State(mut pooler): State<WorkspacePooler>,
|
||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
// FIXME: Check workspace authorization.
|
let mut workspace_client = pooler
|
||||||
// FIXME ensure workspace corresponds to rel/portal, and that user has
|
.acquire_for(workspace_id, RoleAssignment::User(user.id))
|
||||||
// permission to access/alter both as needed.
|
.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)
|
let workspace = Workspace::with_id(workspace_id)
|
||||||
.fetch_one(&mut app_db)
|
.fetch_one(&mut app_db)
|
||||||
.await?;
|
.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)]
|
#[derive(Debug, Template)]
|
||||||
#[template(path = "relations_single/portal_settings.html")]
|
#[template(path = "relations_single/portal_settings.html")]
|
||||||
struct ResponseTemplate {
|
struct ResponseTemplate {
|
||||||
navigator: Navigator,
|
navigator: Navigator,
|
||||||
portal: Portal,
|
portal: Portal,
|
||||||
rel: PgClass,
|
|
||||||
settings: Settings,
|
settings: Settings,
|
||||||
workspace_nav: WorkspaceNav,
|
workspace_nav: WorkspaceNav,
|
||||||
}
|
}
|
||||||
|
|
@ -79,7 +82,6 @@ pub(super) async fn get(
|
||||||
.build()?,
|
.build()?,
|
||||||
navigator,
|
navigator,
|
||||||
portal,
|
portal,
|
||||||
rel,
|
|
||||||
settings,
|
settings,
|
||||||
}
|
}
|
||||||
.render()?,
|
.render()?,
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,10 @@ use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
response::Response,
|
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 interim_pgtypes::{escape_identifier, pg_class::PgClass};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use sqlx::{postgres::types::Oid, query};
|
use sqlx::{postgres::types::Oid, query};
|
||||||
|
|
@ -56,15 +59,34 @@ pub(super) async fn post(
|
||||||
) -> Result<Response, AppError> {
|
) -> Result<Response, AppError> {
|
||||||
// FIXME CSRF
|
// 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.
|
// 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)
|
let field = Field::belonging_to_portal(portal_id)
|
||||||
.with_id(field_id)
|
.with_id(field_id)
|
||||||
.fetch_one(&mut app_db)
|
.fetch_one(&mut app_db)
|
||||||
.await?;
|
.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('_') {
|
if delete_data == "true" && field.name.starts_with('_') {
|
||||||
return Err(bad_request!("cannot delete data for a system column"));
|
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?;
|
field.delete(&mut app_db).await?;
|
||||||
|
|
||||||
if delete_data == "true" {
|
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!(
|
query(&format!(
|
||||||
"alter table {ident} drop column if exists {col_esc}",
|
"alter table {ident} drop column if exists {col_esc}",
|
||||||
ident = rel.get_identifier(),
|
ident = rel.get_identifier(),
|
||||||
|
|
|
||||||
|
|
@ -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:
|
// [`axum_extra`]'s form extractor is preferred:
|
||||||
// https://docs.rs/axum-extra/0.10.1/axum_extra/extract/struct.Form.html#differences-from-axumextractform
|
// https://docs.rs/axum-extra/0.10.1/axum_extra/extract/struct.Form.html#differences-from-axumextractform
|
||||||
use axum_extra::extract::Form;
|
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 serde::Deserialize;
|
||||||
use sqlx::postgres::types::Oid;
|
use sqlx::postgres::types::Oid;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
@ -12,6 +20,7 @@ use crate::{
|
||||||
errors::AppError,
|
errors::AppError,
|
||||||
navigator::{Navigator, NavigatorPage as _},
|
navigator::{Navigator, NavigatorPage as _},
|
||||||
user::CurrentUser,
|
user::CurrentUser,
|
||||||
|
workspace_pooler::{RoleAssignment, WorkspacePooler},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
|
@ -34,7 +43,8 @@ pub(super) struct FormBody {
|
||||||
#[debug_handler(state = App)]
|
#[debug_handler(state = App)]
|
||||||
pub(super) async fn post(
|
pub(super) async fn post(
|
||||||
AppDbConn(mut app_db): AppDbConn,
|
AppDbConn(mut app_db): AppDbConn,
|
||||||
CurrentUser(_user): CurrentUser,
|
State(mut pooler): State<WorkspacePooler>,
|
||||||
|
CurrentUser(user): CurrentUser,
|
||||||
navigator: Navigator,
|
navigator: Navigator,
|
||||||
Path(PathParams {
|
Path(PathParams {
|
||||||
portal_id,
|
portal_id,
|
||||||
|
|
@ -43,13 +53,26 @@ pub(super) async fn post(
|
||||||
}): Path<PathParams>,
|
}): Path<PathParams>,
|
||||||
Form(form): Form<FormBody>,
|
Form(form): Form<FormBody>,
|
||||||
) -> Result<Response, AppError> {
|
) -> Result<Response, AppError> {
|
||||||
// FIXME: Check workspace authorization.
|
// FIXME: csrf
|
||||||
// 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 portal = Portal::with_id(portal_id).fetch_one(&mut app_db).await?;
|
|
||||||
let filter: Option<PgExpressionAny> =
|
let filter: Option<PgExpressionAny> =
|
||||||
serde_json::from_str(&form.filter_expression.unwrap_or("null".to_owned()))?;
|
serde_json::from_str(&form.filter_expression.unwrap_or("null".to_owned()))?;
|
||||||
|
|
||||||
Portal::update()
|
Portal::update()
|
||||||
.id(portal.id)
|
.id(portal.id)
|
||||||
.table_filter(filter)
|
.table_filter(filter)
|
||||||
|
|
@ -61,7 +84,7 @@ pub(super) async fn post(
|
||||||
.portal_page()
|
.portal_page()
|
||||||
.workspace_id(workspace_id)
|
.workspace_id(workspace_id)
|
||||||
.rel_oid(Oid(rel_oid))
|
.rel_oid(Oid(rel_oid))
|
||||||
.portal_id(portal_id)
|
.portal_id(portal.id)
|
||||||
.build()?
|
.build()?
|
||||||
.redirect_to())
|
.redirect_to())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,10 @@ use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
response::Response,
|
response::Response,
|
||||||
};
|
};
|
||||||
use interim_models::portal::{Portal, RE_PORTAL_NAME};
|
use interim_models::{
|
||||||
use interim_pgtypes::pg_class::PgClass;
|
accessors::{Accessor, Actor, portal::PortalAccessor},
|
||||||
|
portal::{Portal, RE_PORTAL_NAME},
|
||||||
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use sqlx::postgres::types::Oid;
|
use sqlx::postgres::types::Oid;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
@ -46,8 +48,6 @@ pub(super) async fn post(
|
||||||
}): Path<PathParams>,
|
}): Path<PathParams>,
|
||||||
ValidatedForm(FormBody { name }): ValidatedForm<FormBody>,
|
ValidatedForm(FormBody { name }): ValidatedForm<FormBody>,
|
||||||
) -> Result<Response, AppError> {
|
) -> Result<Response, AppError> {
|
||||||
// FIXME: Check workspace authorization.
|
|
||||||
|
|
||||||
let mut workspace_client = pooler
|
let mut workspace_client = pooler
|
||||||
.acquire_for(
|
.acquire_for(
|
||||||
workspace_id,
|
workspace_id,
|
||||||
|
|
@ -55,15 +55,19 @@ pub(super) async fn post(
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let rel = PgClass::with_oid(Oid(rel_oid))
|
let portal = PortalAccessor::new()
|
||||||
.fetch_one(&mut workspace_client)
|
.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?;
|
.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()
|
Portal::update()
|
||||||
.id(portal_id)
|
.id(portal.id)
|
||||||
.name(name)
|
.name(name)
|
||||||
.build()?
|
.build()?
|
||||||
.execute(&mut app_db)
|
.execute(&mut app_db)
|
||||||
|
|
@ -73,7 +77,7 @@ pub(super) async fn post(
|
||||||
.portal_page()
|
.portal_page()
|
||||||
.workspace_id(workspace_id)
|
.workspace_id(workspace_id)
|
||||||
.rel_oid(Oid(rel_oid))
|
.rel_oid(Oid(rel_oid))
|
||||||
.portal_id(portal_id)
|
.portal_id(portal.id)
|
||||||
.suffix("settings/")
|
.suffix("settings/")
|
||||||
.build()?
|
.build()?
|
||||||
.redirect_to())
|
.redirect_to())
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,10 @@ use axum::{
|
||||||
// https://docs.rs/axum-extra/0.10.1/axum_extra/extract/struct.Form.html#differences-from-axumextractform
|
// https://docs.rs/axum-extra/0.10.1/axum_extra/extract/struct.Form.html#differences-from-axumextractform
|
||||||
use axum_extra::extract::Form;
|
use axum_extra::extract::Form;
|
||||||
use interim_models::{
|
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 serde::Deserialize;
|
||||||
use sqlx::postgres::types::Oid;
|
use sqlx::postgres::types::Oid;
|
||||||
|
|
@ -49,28 +52,39 @@ pub(super) async fn post(
|
||||||
}): Path<PathParams>,
|
}): Path<PathParams>,
|
||||||
Form(form): Form<HashMap<String, String>>,
|
Form(form): Form<HashMap<String, String>>,
|
||||||
) -> Result<Response, AppError> {
|
) -> Result<Response, AppError> {
|
||||||
// FIXME: Check workspace authorization.
|
|
||||||
// FIXME ensure workspace corresponds to rel/portal, and that user has
|
|
||||||
// permission to access/alter both as needed.
|
|
||||||
// FIXME CSRF
|
// 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
|
let mut workspace_client = workspace_pooler
|
||||||
.acquire_for(workspace.id, RoleAssignment::User(user.id))
|
.acquire_for(workspace_id, RoleAssignment::User(user.id))
|
||||||
.await?;
|
.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 {
|
for (name, content) in form {
|
||||||
let mut name_split = name.split('.');
|
let mut name_split = name.split('.');
|
||||||
let field_id = name_split
|
let field_id = name_split
|
||||||
.next()
|
.next()
|
||||||
.and_then(|value| Uuid::parse_str(value).ok())
|
.and_then(|value| Uuid::parse_str(value).ok())
|
||||||
.ok_or(bad_request!("expected input name to start with <FIELD_ID>"))?;
|
.ok_or(bad_request!("expected input name to start with <FIELD_ID>"))?;
|
||||||
|
|
||||||
|
// For authorization.
|
||||||
|
let _field = Field::belonging_to_portal(portal.id)
|
||||||
|
.with_id(field_id)
|
||||||
|
.fetch_one(&mut app_db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let language = name_split
|
let language = name_split
|
||||||
.next()
|
.next()
|
||||||
.and_then(|value| Language::from_str(value).ok())
|
.and_then(|value| Language::from_str(value).ok())
|
||||||
|
|
@ -95,6 +109,7 @@ pub(super) async fn post(
|
||||||
.workspace_id(workspace_id)
|
.workspace_id(workspace_id)
|
||||||
.rel_oid(Oid(rel_oid))
|
.rel_oid(Oid(rel_oid))
|
||||||
.portal_id(portal_id)
|
.portal_id(portal_id)
|
||||||
|
.suffix("form/")
|
||||||
.build()?
|
.build()?
|
||||||
.redirect_to())
|
.redirect_to())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,13 @@ use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
response::{IntoResponse as _, Response},
|
response::{IntoResponse as _, Response},
|
||||||
};
|
};
|
||||||
use interim_models::{datum::Datum, portal::Portal, workspace::Workspace};
|
use interim_models::{
|
||||||
use interim_pgtypes::{escape_identifier, pg_attribute::PgAttribute, pg_class::PgClass};
|
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::Deserialize;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use sqlx::{Acquire as _, postgres::types::Oid, query};
|
use sqlx::{Acquire as _, postgres::types::Oid, query};
|
||||||
|
|
@ -54,28 +59,32 @@ pub(super) async fn post(
|
||||||
}): Path<PathParams>,
|
}): Path<PathParams>,
|
||||||
Json(form): Json<FormBody>,
|
Json(form): Json<FormBody>,
|
||||||
) -> Result<Response, AppError> {
|
) -> Result<Response, AppError> {
|
||||||
// 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.
|
// Prevent users from modifying Phonograph metadata columns.
|
||||||
if form.cells.iter().any(|cell| cell.column.starts_with('_')) {
|
if form.cells.iter().any(|cell| cell.column.starts_with('_')) {
|
||||||
return Err(forbidden!("access denied to update system metadata column"));
|
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
|
let mut workspace_client = workspace_pooler
|
||||||
.acquire_for(workspace.id, RoleAssignment::User(user.id))
|
.acquire_for(workspace_id, RoleAssignment::User(user.id))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let rel = PgClass::with_oid(portal.class_oid)
|
let rel = PgClass::with_oid(Oid(rel_oid))
|
||||||
.fetch_one(&mut workspace_client)
|
.fetch_one(&mut workspace_client)
|
||||||
.await?;
|
.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))
|
let pkey_attrs = PgAttribute::pkeys_for_rel(Oid(rel_oid))
|
||||||
.fetch_all(&mut workspace_client)
|
.fetch_all(&mut workspace_client)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,13 @@ use interim_models::{
|
||||||
client::AppDbClient, cluster::Cluster, user::User, workspace::Workspace,
|
client::AppDbClient, cluster::Cluster, user::User, workspace::Workspace,
|
||||||
workspace_user_perm::WorkspaceMembership,
|
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 sqlx::{Connection as _, PgConnection, query};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::AppDbConn,
|
app::AppDbConn,
|
||||||
errors::AppError,
|
errors::AppError,
|
||||||
navigator::{Navigator, NavigatorPage as _},
|
navigator::{Navigator, NavigatorPage as _},
|
||||||
roles::ROLE_PREFIX_USER,
|
|
||||||
settings::Settings,
|
|
||||||
user::CurrentUser,
|
user::CurrentUser,
|
||||||
workspace_pooler::{RoleAssignment, WorkspacePooler},
|
workspace_pooler::{RoleAssignment, WorkspacePooler},
|
||||||
workspace_utils::PHONO_TABLE_NAMESPACE,
|
workspace_utils::PHONO_TABLE_NAMESPACE,
|
||||||
|
|
@ -22,7 +20,6 @@ use crate::{
|
||||||
/// the human-friendly name may be changed later rather than specified
|
/// the human-friendly name may be changed later rather than specified
|
||||||
/// permenantly upon creation.
|
/// permenantly upon creation.
|
||||||
pub(super) async fn post(
|
pub(super) async fn post(
|
||||||
State(settings): State<Settings>,
|
|
||||||
State(mut pooler): State<WorkspacePooler>,
|
State(mut pooler): State<WorkspacePooler>,
|
||||||
AppDbConn(mut app_db): AppDbConn,
|
AppDbConn(mut app_db): AppDbConn,
|
||||||
navigator: Navigator,
|
navigator: Navigator,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,10 @@ use axum::{
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
};
|
};
|
||||||
use interim_models::{service_cred::ServiceCred, workspace::Workspace};
|
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 rand::distributions::{Alphanumeric, DistString};
|
||||||
use redact::Secret;
|
use redact::Secret;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
@ -15,7 +18,6 @@ use crate::{
|
||||||
app::{App, AppDbConn},
|
app::{App, AppDbConn},
|
||||||
errors::AppError,
|
errors::AppError,
|
||||||
navigator::{Navigator, NavigatorPage},
|
navigator::{Navigator, NavigatorPage},
|
||||||
roles::{ROLE_PREFIX_SERVICE_CRED, SERVICE_CRED_CONN_LIMIT, SERVICE_CRED_SUFFIX_LEN},
|
|
||||||
user::CurrentUser,
|
user::CurrentUser,
|
||||||
workspace_pooler::{RoleAssignment, WorkspacePooler},
|
workspace_pooler::{RoleAssignment, WorkspacePooler},
|
||||||
workspace_utils::PHONO_TABLE_NAMESPACE,
|
workspace_utils::PHONO_TABLE_NAMESPACE,
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,13 @@ use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
response::IntoResponse,
|
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 serde::Deserialize;
|
||||||
use sqlx::query;
|
use sqlx::query;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
@ -10,10 +16,6 @@ use uuid::Uuid;
|
||||||
use crate::{
|
use crate::{
|
||||||
errors::AppError,
|
errors::AppError,
|
||||||
navigator::{Navigator, NavigatorPage as _},
|
navigator::{Navigator, NavigatorPage as _},
|
||||||
roles::{
|
|
||||||
ROLE_PREFIX_TABLE_OWNER, ROLE_PREFIX_TABLE_READER, ROLE_PREFIX_TABLE_WRITER,
|
|
||||||
ROLE_PREFIX_USER,
|
|
||||||
},
|
|
||||||
user::CurrentUser,
|
user::CurrentUser,
|
||||||
workspace_pooler::{RoleAssignment, WorkspacePooler},
|
workspace_pooler::{RoleAssignment, WorkspacePooler},
|
||||||
workspace_utils::PHONO_TABLE_NAMESPACE,
|
workspace_utils::PHONO_TABLE_NAMESPACE,
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,12 @@ use anyhow::Result;
|
||||||
use axum::extract::FromRef;
|
use axum::extract::FromRef;
|
||||||
use derive_builder::Builder;
|
use derive_builder::Builder;
|
||||||
use interim_models::{client::AppDbClient, workspace::Workspace};
|
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 sqlx::{Executor, PgPool, postgres::PgPoolOptions, raw_sql};
|
||||||
use tokio::sync::{OnceCell, RwLock};
|
use tokio::sync::{OnceCell, RwLock};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{app::App, roles::ROLE_PREFIX_USER};
|
use crate::app::App;
|
||||||
|
|
||||||
const MAX_CONNECTIONS: u32 = 4;
|
const MAX_CONNECTIONS: u32 = 4;
|
||||||
const IDLE_SECONDS: u64 = 3600;
|
const IDLE_SECONDS: u64 = 3600;
|
||||||
|
|
@ -109,15 +109,15 @@ discard sequences;
|
||||||
|
|
||||||
pub async fn close_for(&mut self, base_id: Uuid) -> Result<()> {
|
pub async fn close_for(&mut self, base_id: Uuid) -> Result<()> {
|
||||||
let pools = self.pools.read().await;
|
let pools = self.pools.read().await;
|
||||||
if let Some(cell) = pools.get(&base_id) {
|
if let Some(cell) = pools.get(&base_id)
|
||||||
if let Some(pool) = cell.get() {
|
&& let Some(pool) = cell.get()
|
||||||
let pool = pool.clone();
|
{
|
||||||
drop(pools); // Release read lock
|
let pool = pool.clone();
|
||||||
let mut pools = self.pools.write().await;
|
drop(pools); // Release read lock
|
||||||
pools.remove(&base_id);
|
let mut pools = self.pools.write().await;
|
||||||
drop(pools); // Release write lock
|
pools.remove(&base_id);
|
||||||
pool.close().await;
|
drop(pools); // Release write lock
|
||||||
}
|
pool.close().await;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue