test: permissions

This commit is contained in:
Orion Kindel 2023-07-14 13:53:11 -05:00
parent 3722a9ce2f
commit 50f416d402
Signed by untrusted user who does not match committer: orion
GPG Key ID: 6D4165AE4C928719
5 changed files with 128 additions and 79 deletions

View File

@ -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<str>
{
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) })
}
}

View File

@ -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::<String>(ps[1]), String::from("XXX"));
Ok(Row::new(vec![("", Type::BOOL)]).value(Type::BOOL, from_sql_owned::<String>(ps[0]) == *"foo"))
Ok(Row::new(vec![("",
Type::BOOL,
Box::new(from_sql_owned::<String>(ps[0])
== *"foo"))]))
}),
..Client::default() };

View File

@ -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<str>
{
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::<String, _, _, _>(Some("perm"), "owner", row))
.zip(|_: &_| Self::try_get::<String, _, _, _>(Some("perm"), "group", row))
.zip(|_: &_| Self::try_get::<String, _, _, _>(Some("perm"), "path", row))
.zip(|_: &_| try_get!(<String> Some("perm"), "owner", row))
.zip(|_: &_| try_get!(<String> Some("perm"), "group", row))
.zip(|_: &_| try_get!(<String> 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));
}
}

View File

@ -10,19 +10,20 @@ use crate::perm::Actor;
pub type DbError<Pg> = <<Pg as Postgres>::Client as postgres::GenericClient>::Error;
pub trait UnmarshalRow: Sized + Send + Sync {
fn try_get<'a, T, R, S1, S2>(prefix: Option<S1>, col: S2, row: &'a R) -> Result<T, R::Error>
where R: GenericRow,
S1: AsRef<str>,
S2: AsRef<str>,
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<R, S>(col_prefix: Option<S>, row: &R) -> Result<Self, R::Error>
where R: GenericRow,
S: AsRef<str>;
@ -210,22 +211,19 @@ pub mod test {
}
impl<E> Row<E> {
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<dyn ToSql>)>) -> 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<V>(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<E> GenericRow for Row<E> 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() };

View File

@ -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<str>
{
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::<String, _, _, _>(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!(<String> 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::<UserTag>::new()),
query_one: Box::new(|c, q, ps| {
let tags = c.state_mut::<Vec<UserTag>>();
let client =
|| Client::<()> { state: Box::new(Vec::<UserTag>::new()),
query_one: Box::new(|c, q, ps| {
let tags = c.state_mut::<Vec<UserTag>>();
let tag = UserTag::from(from_sql_owned::<String>(ps[0]));
let tag = UserTag::from(from_sql_owned::<String>(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();