use std::collections::HashSet; use anyhow::anyhow; use askama::Template; 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; // TODO: custom error type // TODO: make params and result references fn get_table_role( relacl: Option>, required_privileges: HashSet, disallowed_privileges: HashSet, role_prefix: &str, ) -> Result { let mut roles: Vec = vec![]; dbg!(&relacl); for acl_item in relacl.ok_or(anyhow!("acl not present on class"))? { if acl_item.grantee.starts_with(role_prefix) { let privileges_set: HashSet = acl_item .privileges .iter() .map(|privilege| privilege.privilege) .collect(); assert!( privileges_set.intersection(&required_privileges).count() == required_privileges.len() ); assert!(privileges_set.intersection(&disallowed_privileges).count() == 0); roles.push(acl_item.grantee) } } assert!(roles.len() == 1); Ok(roles .first() .expect("already asserted that `roles` has len 1") .clone()) } /// Returns the name of the "table_reader" role created by Phonograph for a /// particular workspace table. The role is assessed based on its name and the /// table permissions directly granted to it. Returns an error if no matching /// role is found, and panics if a role is found with excess permissions /// granted to it directly. pub(crate) fn get_reader_role(rel: &PgClass) -> Result { get_table_role( rel.relacl.clone(), [PgPrivilegeType::Select].into(), [ PgPrivilegeType::Insert, PgPrivilegeType::Update, PgPrivilegeType::Delete, PgPrivilegeType::Truncate, ] .into(), ROLE_PREFIX_TABLE_READER, ) } /// Returns the name of the "table_writer" role created by Phonograph for a /// particular workspace table. The role is assessed based on its name and the /// table permissions directly granted to it. Returns an error if no matching /// role is found, and panics if a role is found with excess permissions /// granted to it directly. pub(crate) fn get_writer_role(rel: &PgClass) -> Result { get_table_role( rel.relacl.clone(), [PgPrivilegeType::Delete, PgPrivilegeType::Truncate].into(), [PgPrivilegeType::Select].into(), ROLE_PREFIX_TABLE_WRITER, ) } #[derive(Clone, Debug, Deserialize, Template, Serialize)] #[template(path = "role_display.html")] pub(crate) enum RoleDisplay { TableOwner { oid: Oid, relname: String }, TableReader { oid: Oid, relname: String }, TableWriter { oid: Oid, relname: String }, Unknown { rolname: String }, } impl RoleDisplay { /// Attempt to infer value from a role name in a specific workspace. If the /// role corresponds specifically to a relation and that relation is not /// present in the current workspace, the returned value is `None`. pub async fn from_rolname( rolname: &str, client: &mut WorkspaceClient, ) -> sqlx::Result> { if rolname.starts_with(ROLE_PREFIX_TABLE_OWNER) || rolname.starts_with(ROLE_PREFIX_TABLE_READER) || rolname.starts_with(ROLE_PREFIX_TABLE_WRITER) { #[derive(FromRow)] struct RelInfo { oid: Oid, relname: String, } // TODO: Consider moving this to [`interim-pgtypes`]. let mut rels: Vec = query_as( r#" select oid, any_value(relname) as relname from ( select oid, relname, (aclexplode(relacl)).grantee as grantee from pg_class ) where grantee = $1::regrole::oid group by oid "#, ) .bind(rolname) .fetch_all(client.get_conn()) .await?; assert!(rels.len() <= 1); Ok(rels.pop().map(|rel| { if rolname.starts_with(ROLE_PREFIX_TABLE_OWNER) { Self::TableOwner { oid: rel.oid, relname: rel.relname, } } else if rolname.starts_with(ROLE_PREFIX_TABLE_READER) { Self::TableReader { oid: rel.oid, relname: rel.relname, } } else { Self::TableWriter { oid: rel.oid, relname: rel.relname, } } })) } else { Ok(Some(Self::Unknown { rolname: rolname.to_owned(), })) } } }