From 50f416d402cb1529c677c040a568c3d670450b5e Mon Sep 17 00:00:00 2001 From: Orion Kindel Date: Fri, 14 Jul 2023 13:53:11 -0500 Subject: [PATCH] test: permissions --- src/group.rs | 8 ++--- src/hashed_text.rs | 5 ++- src/perm.rs | 86 +++++++++++++++++++++++++++++++++++++--------- src/postgres.rs | 50 +++++++++++++-------------- src/user.rs | 58 +++++++++++++++---------------- 5 files changed, 128 insertions(+), 79 deletions(-) diff --git a/src/group.rs b/src/group.rs index a1cd0ea..697dccc 100644 --- a/src/group.rs +++ b/src/group.rs @@ -3,7 +3,7 @@ use postgres::GenericClient; use crate::newtype; use crate::perm::Actor; -use crate::postgres::{DbError, Postgres, UnmarshalRow}; +use crate::postgres::{try_get, DbError, Postgres, UnmarshalRow}; use crate::repo::Repo; use crate::user::{User, UserId}; @@ -29,9 +29,9 @@ impl UnmarshalRow for Group { S: AsRef { let p = p.as_ref().map(|p| p.as_ref()); - Self::try_get(p, "uid", row).zip(|_: &_| Self::try_get(p, "name", row)) - .map(|(uid, name)| Group { uid: GroupId(uid), - name: GroupName(name) }) + try_get!(p, "uid", row).zip(|_: &_| try_get!(p, "name", row)) + .map(|(uid, name)| Group { uid: GroupId(uid), + name: GroupName(name) }) } } diff --git a/src/hashed_text.rs b/src/hashed_text.rs index 8eaead3..2a06ea4 100644 --- a/src/hashed_text.rs +++ b/src/hashed_text.rs @@ -45,7 +45,10 @@ mod test { assert_eq!(q.unwrap_str(), "select public.hashed_text_matches($1, public.hashed_text_of_string($2))"); assert_eq!(from_sql_owned::(ps[1]), String::from("XXX")); - Ok(Row::new(vec![("", Type::BOOL)]).value(Type::BOOL, from_sql_owned::(ps[0]) == *"foo")) + Ok(Row::new(vec![("", + Type::BOOL, + Box::new(from_sql_owned::(ps[0]) + == *"foo"))])) }), ..Client::default() }; diff --git a/src/perm.rs b/src/perm.rs index 7ca00be..af2cbe7 100644 --- a/src/perm.rs +++ b/src/perm.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use naan::prelude::*; use crate::group::{Group, GroupId}; -use crate::postgres::UnmarshalRow; +use crate::postgres::{try_get, UnmarshalRow}; use crate::user::UserId; #[derive(Default, Clone, PartialEq, Eq, Debug)] @@ -133,19 +133,20 @@ impl UnmarshalRow for Mode { S: AsRef { let p = p.as_ref().map(|p| p.as_ref()); - Self::try_get(p, "read", row).zip(|_: &_| Self::try_get(p, "write", row)) - .map(|(r, w)| { - if w { - Mode::Write - } else if r { - Mode::Read - } else { - Mode::None - } - }) + try_get!(p, "read", row).zip(|_: &_| try_get!(p, "write", row)) + .map(|(r, w)| { + if w { + Mode::Write + } else if r { + Mode::Read + } else { + Mode::None + } + }) } } +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct Perm { pub path: Path, pub owner: (UserId, Mode), @@ -167,9 +168,9 @@ impl UnmarshalRow for Perm { Mode::unmarshal_prefixed("owner_mode", row) .zip(|_: &_| Mode::unmarshal_prefixed("group_mode", row)) .zip(|_: &_| Mode::unmarshal_prefixed("everyone_mode", row)) - .zip(|_: &_| Self::try_get::(Some("perm"), "owner", row)) - .zip(|_: &_| Self::try_get::(Some("perm"), "group", row)) - .zip(|_: &_| Self::try_get::(Some("perm"), "path", row)) + .zip(|_: &_| try_get!( Some("perm"), "owner", row)) + .zip(|_: &_| try_get!( Some("perm"), "group", row)) + .zip(|_: &_| try_get!( Some("perm"), "path", row)) .map(|(((((owner_mode, group_mode), everyone_mode), owner), group), path)| Perm { path: Path::parse(path), owner: (UserId::from(owner), owner_mode), @@ -181,15 +182,19 @@ impl UnmarshalRow for Perm { impl Perm { pub fn actor_can(&self, actor: &Actor, mode: Mode) -> bool { - actor.uid.as_ref() == Some(&self.owner.0) - || actor.groups.iter().any(|g| g.uid == self.group.0) - || mode.covered_by(&self.everyone) + mode.covered_by(&self.everyone) + || actor.uid.as_ref() == Some(&self.owner.0) && mode.covered_by(&self.owner.1) + || mode.covered_by(&self.group.1) && actor.groups.iter().any(|g| g.uid == self.group.0) } } #[cfg(test)] mod tests { + use postgres::types::Type; + use super::*; + use crate::group::GroupName; + use crate::postgres::test::Row; #[test] fn path_parse() { @@ -208,4 +213,51 @@ mod tests { assert_eq!(Path::parse("/groups/1/members"), Path::Group(Dir::Within(GroupId::from("1"), GroupPath::Members))); } + + #[test] + fn perm_unmarshal() { + let row = Row::<()>::new(vec![("owner_mode.read", Type::BOOL, Box::new(true)), + ("owner_mode.write", Type::BOOL, Box::new(true)), + ("group_mode.read", Type::BOOL, Box::new(true)), + ("group_mode.write", Type::BOOL, Box::new(true)), + ("everyone_mode.read", Type::BOOL, Box::new(true)), + ("everyone_mode.write", Type::BOOL, Box::new(false)), + ("perm.owner", Type::TEXT, Box::new("u1")), + ("perm.group", Type::TEXT, Box::new("g1")), + ("perm.path", Type::TEXT, Box::new("/groups/")),]); + + let perm = Perm::unmarshal(&row).unwrap(); + assert_eq!(perm, + Perm { everyone: Mode::Read, + group: (GroupId::from("g1"), Mode::Write), + owner: (UserId::from("u1"), Mode::Write), + path: Path::Group(Dir::Dir) }); + } + + #[test] + fn actor_can() { + let all_groups = Perm { everyone: Mode::Write, + group: (GroupId::from("g1"), Mode::Write), + owner: (UserId::from("u1"), Mode::Write), + path: Path::Group(Dir::Dir) }; + + let group_admin_members = Perm { everyone: Mode::Read, + group: (GroupId::from("g1"), Mode::Write), + owner: (UserId::from("u1"), Mode::Write), + path: Path::Group(Dir::Within(GroupId::from("g1"), + GroupPath::Members)) }; + + let admin = Actor { uid: Some(UserId::from("u1")), + groups: vec![Group { uid: GroupId::from("g1"), + name: GroupName::from("admins") }] }; + + let nonadmin = Actor { uid: Some(UserId::from("u2")), + groups: vec![Group { uid: GroupId::from("g2"), + name: GroupName::from("foo") }] }; + + assert!(all_groups.actor_can(&admin, Mode::Write)); + assert!(all_groups.actor_can(&nonadmin, Mode::Write)); + assert!(group_admin_members.actor_can(&admin, Mode::Write)); + assert!(!group_admin_members.actor_can(&nonadmin, Mode::Write)); + } } diff --git a/src/postgres.rs b/src/postgres.rs index 96a7cb2..dd44afa 100644 --- a/src/postgres.rs +++ b/src/postgres.rs @@ -10,19 +10,20 @@ use crate::perm::Actor; pub type DbError = <::Client as postgres::GenericClient>::Error; -pub trait UnmarshalRow: Sized + Send + Sync { - fn try_get<'a, T, R, S1, S2>(prefix: Option, col: S2, row: &'a R) -> Result - where R: GenericRow, - S1: AsRef, - S2: AsRef, - T: FromSql<'a> - { - let col = prefix.filter(|s| !s.as_ref().is_empty()) - .map(|s| format!("{}.{}", s.as_ref(), col.as_ref())) - .unwrap_or(col.as_ref().into()); - row.try_get::<_, T>(col.as_str()) - } +macro_rules! try_get { + (<$t:ty> $p:expr, $col:expr, $row:expr) => {{ + let col = $p.filter(|s| !str::is_empty(s.as_ref())) + .map(|s| format!("{}.{}", s, $col)) + .unwrap_or($col.to_string()); + $row.try_get::<_, $t>(col.as_str()) + }}; + ($p:expr, $col:expr, $row:expr) => {{ + try_get!(<_> $p, $col, $row) + }}; +} +pub(crate) use try_get; +pub trait UnmarshalRow: Sized + Send + Sync { fn unmarshal_maybe_prefixed(col_prefix: Option, row: &R) -> Result where R: GenericRow, S: AsRef; @@ -210,22 +211,19 @@ pub mod test { } impl Row { - pub fn new(cols: Vec<(&'static str, Type)>) -> Self { - Self { columns: cols.into_iter() - .map(|(name, ty)| Column::new(name.to_string(), ty)) + pub fn new(cols: Vec<(&'static str, Type, Box)>) -> Self { + Self { columns: cols.iter() + .map(|(name, ty, _)| Column::new(name.to_string(), ty.clone())) .collect(), - values: vec![], + values: cols.into_iter() + .map(|(_, ty, val)| { + let mut bs = BytesMut::with_capacity(128); + val.to_sql(&ty, &mut bs).unwrap(); + bs + }) + .collect(), __phantom: PhantomData } } - - pub fn value(mut self, ty: Type, val: V) -> Self - where V: ToSql - { - let mut bs = BytesMut::with_capacity(128); - val.to_sql(&ty, &mut bs).unwrap(); - self.values.push(bs); - self - } } impl GenericRow for Row where E: core::fmt::Debug @@ -451,7 +449,7 @@ pub mod test { #[test] fn postgres_impl_pooling() { let client = || Client { query_one: Box::new(|_, _, _| { - Ok(Row::new(vec![("foo", Type::TEXT)]).value(Type::TEXT, "bar")) + Ok(Row::new(vec![("foo", Type::TEXT, Box::new("bar"))])) }), ..Client::default() }; diff --git a/src/user.rs b/src/user.rs index 079bc16..9b2305b 100644 --- a/src/user.rs +++ b/src/user.rs @@ -3,7 +3,7 @@ use postgres::{GenericClient, GenericRow}; use crate::hashed_text::HashedText; use crate::perm::Actor; -use crate::postgres::{DbError, Postgres, UnmarshalRow}; +use crate::postgres::{try_get, DbError, Postgres, UnmarshalRow}; use crate::repo::Repo; use crate::{newtype, Email}; @@ -31,15 +31,15 @@ impl UnmarshalRow for User { S: AsRef { let p = p.as_ref().map(|p| p.as_ref()); - Self::try_get(p, "uid", row).zip(|_: &_| Self::try_get(p, "tag", row)) - .zip(|_: &_| Self::try_get::(p, "password", row)) - .zip(|_: &_| Self::try_get(p, "email", row)) - .map(|(((uid, tag), password), email)| { - User { uid: UserId(uid), - tag: UserTag(tag), - password: HashedText::from(password), - email: Email(email) } - }) + try_get!(p, "uid", row).zip(|_: &_| try_get!(p, "tag", row)) + .zip(|_: &_| try_get!( p, "password", row)) + .zip(|_: &_| try_get!(p, "email", row)) + .map(|(((uid, tag), password), email)| { + User { uid: UserId(uid), + tag: UserTag(tag), + password: HashedText::from(password), + email: Email(email) } + }) } } @@ -154,13 +154,10 @@ mod tests { use crate::Email; fn usr_row(usr: User) -> Row<()> { - Row::new(vec![("uid", Type::UUID), - ("email", Type::TEXT), - ("password", Type::TEXT), - ("tag", Type::TEXT)]).value(Type::UUID, usr.uid.to_string()) - .value(Type::TEXT, usr.email.to_string()) - .value(Type::TEXT, usr.password.to_string()) - .value(Type::TEXT, usr.tag.to_string()) + Row::new(vec![("uid", Type::UUID, Box::new(usr.uid.to_string())), + ("email", Type::TEXT, Box::new(usr.email.to_string())), + ("password", Type::TEXT, Box::new(usr.password.to_string())), + ("tag", Type::TEXT, Box::new(usr.tag.to_string()))]) } #[test] @@ -230,22 +227,21 @@ mod tests { #[test] fn user_repo_insert() { - let client = || Client::<()> { state: Box::new(Vec::::new()), - query_one: Box::new(|c, q, ps| { - let tags = c.state_mut::>(); + let client = + || Client::<()> { state: Box::new(Vec::::new()), + query_one: Box::new(|c, q, ps| { + let tags = c.state_mut::>(); - let tag = UserTag::from(from_sql_owned::(ps[0])); + let tag = UserTag::from(from_sql_owned::(ps[0])); - if tags.contains(&tag) { - Err(()) - } else { - tags.push(tag); - Ok(Row::new(vec![("", Type::TEXT)]).value(Type::TEXT, - tags.len() - .to_string())) - } - }), - ..Client::default() }; + if tags.contains(&tag) { + Err(()) + } else { + tags.push(tag); + Ok(Row::new(vec![("", Type::TEXT, Box::new(tags.len().to_string()))])) + } + }), + ..Client::default() }; let db = PostgresImpl::try_new(|| Ok(client()), 1).unwrap();