151 lines
5 KiB
Rust
151 lines
5 KiB
Rust
use std::collections::HashSet;
|
|
|
|
use anyhow::anyhow;
|
|
use askama::Template;
|
|
use phono_backends::{
|
|
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<Vec<PgAclItem>>,
|
|
required_privileges: HashSet<PgPrivilegeType>,
|
|
disallowed_privileges: HashSet<PgPrivilegeType>,
|
|
role_prefix: &str,
|
|
) -> Result<String, AppError> {
|
|
let mut roles: Vec<String> = 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<PgPrivilegeType> = 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<String, AppError> {
|
|
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<String, AppError> {
|
|
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<Option<Self>> {
|
|
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 [`phono_backends`].
|
|
let mut rels: Vec<RelInfo> = 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(),
|
|
}))
|
|
}
|
|
}
|
|
}
|