feat: add permissions model

This commit is contained in:
Orion Kindel 2023-07-13 19:03:43 -05:00
parent 81f811f8d3
commit 3722a9ce2f
Signed by untrusted user who does not match committer: orion
GPG Key ID: 6D4165AE4C928719
11 changed files with 573 additions and 73 deletions

View File

@ -1,13 +1,37 @@
use crate::hashed_text::{HashedTextExt, HashedTextExtImpl}; use crate::hashed_text::{HashedTextExt, HashedTextExtImpl};
use crate::postgres::PostgresImpl; use crate::postgres::{Postgres, PostgresImpl};
use crate::user::UserRepo; use crate::user::{UserRepo, UserRepoImpl};
trait App: Send + Sync + Sized { pub trait App: Send + Sync + Sized {
type Db: Postgres;
type HashedTextExt: HashedTextExt; type HashedTextExt: HashedTextExt;
type UserRepo: UserRepo; type UserRepo: UserRepo;
fn db(&self) -> &Self::Db;
fn hashed_text(&self) -> &Self::HashedTextExt;
fn user(&self) -> &Self::UserRepo;
} }
pub struct AppConcrete { pub struct AppConcrete {
pg: PostgresImpl<postgres::Client>, pub pg: &'static PostgresImpl<postgres::Client>,
hashed_text_ext: HashedTextExtImpl<PostgresImpl<postgres::Client>>, pub hashed_text_ext: HashedTextExtImpl<PostgresImpl<postgres::Client>>,
pub user: UserRepoImpl<PostgresImpl<postgres::Client>>,
}
impl App for AppConcrete {
type Db = PostgresImpl<postgres::Client>;
type HashedTextExt = HashedTextExtImpl<Self::Db>;
type UserRepo = UserRepoImpl<Self::Db>;
fn db(&self) -> &Self::Db {
&self.pg
}
fn hashed_text(&self) -> &Self::HashedTextExt {
&self.hashed_text_ext
}
fn user(&self) -> &Self::UserRepo {
&self.user
}
} }

View File

@ -1,5 +1,6 @@
use std::ffi::OsString; use std::ffi::OsString;
use std::net::{AddrParseError, SocketAddr}; use std::net::{AddrParseError, SocketAddr};
use std::num::ParseIntError;
use std::str::FromStr; use std::str::FromStr;
use naan::prelude::*; use naan::prelude::*;
@ -15,6 +16,7 @@ pub struct Postgres {
pub pass: String, pub pass: String,
pub host: String, pub host: String,
pub port: String, pub port: String,
pub pool_size: usize,
} }
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
@ -26,7 +28,8 @@ pub struct Env {
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub enum Error { pub enum Error {
VarNotUnicode(String, OsString), VarNotUnicode(String, OsString),
VarNotSocketAddr(String, AddrParseError), VarNotSocketAddr(String, String, AddrParseError),
VarNotInt(String, String, ParseIntError),
RequiredVarNotSet(String), RequiredVarNotSet(String),
} }
@ -48,13 +51,20 @@ impl Env {
let postgres = Postgres { user: get_required("POSTGRES_USER")?, let postgres = Postgres { user: get_required("POSTGRES_USER")?,
host: get_required("POSTGRES_HOST")?, host: get_required("POSTGRES_HOST")?,
pass: get_required("POSTGRES_PASS")?, pass: get_required("POSTGRES_PASS")?,
port: get_required("POSTGRES_PORT")? }; port: get_required("POSTGRES_PORT")?,
pool_size: get("POSTGRES_POOL_SIZE")?.map(|s| {
usize::from_str_radix(&s, 10).map_err(|e| {
Error::VarNotInt("POSTGRES_POOL_SIZE".into(), s, e)
})
})
.unwrap_or(Ok(10))? };
let api_addr = get("API_ADDR")?.unwrap_or("127.0.0.1:4444".into()); let api_addr = get("API_ADDR")?.unwrap_or("127.0.0.1:4444".into());
let api = Api { addr: api_addr.trim() let api =
Api { addr: api_addr.trim()
.parse() .parse()
.map_err(|e| Error::VarNotSocketAddr("API_ADDR".into(), e))? }; .map_err(|e| Error::VarNotSocketAddr("API_ADDR".into(), api_addr, e))? };
Ok(Env { postgres, api }) Ok(Env { postgres, api })
} }

93
src/group.rs Normal file
View File

@ -0,0 +1,93 @@
use naan::prelude::*;
use postgres::GenericClient;
use crate::newtype;
use crate::perm::Actor;
use crate::postgres::{DbError, Postgres, UnmarshalRow};
use crate::repo::Repo;
use crate::user::{User, UserId};
newtype!(
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct GroupId(String);
);
newtype!(
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct GroupName(String);
);
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct Group {
pub uid: GroupId,
pub name: GroupName,
}
impl UnmarshalRow for Group {
fn unmarshal_maybe_prefixed<R, S>(p: Option<S>, row: &R) -> Result<Self, R::Error>
where R: postgres::GenericRow,
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) })
}
}
#[derive(Default, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct GroupPatch {
members: Vec<UserId>,
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct GroupInsert {
name: GroupName,
members: Vec<UserId>,
}
pub trait GroupRepo: Repo<T = Group, Patch = GroupPatch, Id = GroupId> {
fn members(&self, id: &GroupId) -> Result<Vec<UserId>, Self::Error>;
}
pub struct GroupRepoImpl<Db: 'static>(pub &'static Db);
impl<Db> Repo for GroupRepoImpl<Db> where Db: Postgres + 'static
{
type T = Group;
type Patch = GroupPatch;
type Insert = GroupInsert;
type Error = DbError<Db>;
type Id = GroupId;
fn get(&self, actor: &Actor, id: &GroupId) -> Result<Option<Group>, Self::Error> {
static QUERY: &'static str = "select uid, name from public.grp where id = $1 :: uuid";
self.0
.with_client(|c| c.query_opt(QUERY, &[&id.as_ref()]))
.and_then(|opt| {
opt.as_ref()
.map(Group::unmarshal)
.sequence::<hkt::ResultOk<_>>()
})
}
fn get_all(&self, actor: &Actor) -> Result<Vec<Group>, Self::Error> {
static QUERY: &'static str = "select uid, tag, password, email from public.grp";
self.0
.with_client(|c| c.query(QUERY, &[]))
.and_then(|vec| vec.iter().map(Group::unmarshal).collect())
}
fn patch(&self, actor: &Actor, id: &GroupId, state: GroupPatch) -> Result<bool, Self::Error> {
todo!()
}
fn insert(&self, actor: &Actor, state: GroupInsert) -> Result<GroupId, Self::Error> {
todo!()
}
fn del(&self, actor: &Actor, id: &GroupId) -> Result<bool, Self::Error> {
todo!()
}
}

View File

@ -13,7 +13,7 @@ pub trait HashedTextExt: Ext {
fn matches<S: AsRef<str>>(&self, this: &HashedText, other: S) -> Result<bool, Self::Error>; fn matches<S: AsRef<str>>(&self, this: &HashedText, other: S) -> Result<bool, Self::Error>;
} }
pub struct HashedTextExtImpl<Db: Postgres>(&'static Db); pub struct HashedTextExtImpl<Db: Postgres>(pub &'static Db);
impl<Db> Ext for HashedTextExtImpl<Db> where Db: Postgres impl<Db> Ext for HashedTextExtImpl<Db> where Db: Postgres
{ {

View File

@ -1,3 +1,6 @@
use app::{App, AppConcrete};
use hashed_text::{HashedTextExt, HashedTextExtImpl};
use repo::Repo;
use toad::config::Config; use toad::config::Config;
use toad::net::Addrd; use toad::net::Addrd;
use toad::platform::Platform as _; use toad::platform::Platform as _;
@ -6,12 +9,18 @@ use toad::resp::Resp;
use toad_msg::alloc::Message; use toad_msg::alloc::Message;
use toad_msg::Type; use toad_msg::Type;
use crate::postgres::{Postgres, PostgresImpl};
mod app; mod app;
mod env; mod env;
mod group;
mod hashed_text; mod hashed_text;
mod perm;
mod postgres; mod postgres;
mod repo; mod repo;
mod req_payload;
mod user; mod user;
mod user_session;
mod uuid; mod uuid;
#[macro_export] #[macro_export]
@ -100,8 +109,11 @@ mod __toad_aliases {
} }
use __toad_aliases::*; use __toad_aliases::*;
use user::{UserId, UserRepoImpl};
fn handle_request(req: Addrd<Req<ToadT>>) -> Result<Addrd<Message>, String> { fn handle_request<A>(app: &A, req: Addrd<Req<ToadT>>) -> Result<Addrd<Message>, String>
where A: App
{
let path = req.data() let path = req.data()
.path() .path()
.map_err(|e| format!("{e:?}")) .map_err(|e| format!("{e:?}"))
@ -111,7 +123,11 @@ fn handle_request(req: Addrd<Req<ToadT>>) -> Result<Addrd<Message>, String> {
if path_segments.peek() == Some(&"users") { if path_segments.peek() == Some(&"users") {
let mut path_segments = path_segments.clone(); let mut path_segments = path_segments.clone();
let _id = path_segments.nth(2); let id = path_segments.nth(2).map(|s| UserId::from(s));
//match id {
// | Some(id) => app.user().get(id),
// | None => app.users(),
//}
let msg = let msg =
Message::builder(Type::Ack, toad::resp::code::NOT_FOUND).token(req.data().msg().token) Message::builder(Type::Ack, toad::resp::code::NOT_FOUND).token(req.data().msg().token)
.build(); .build();
@ -124,11 +140,13 @@ fn handle_request(req: Addrd<Req<ToadT>>) -> Result<Addrd<Message>, String> {
} }
} }
fn server_worker(p: &'static Toad) { fn server_worker<A>(app: &A, p: &'static Toad)
where A: App
{
loop { loop {
match nb::block!(p.poll_req()) { match nb::block!(p.poll_req()) {
| Err(e) => log::error!("{e:?}"), | Err(e) => log::error!("{e:?}"),
| Ok(req) => match handle_request(req) { | Ok(req) => match handle_request(app, req) {
| Err(e) => log::error!("{e:?}"), | Err(e) => log::error!("{e:?}"),
| Ok(rep) => { | Ok(rep) => {
nb::block!(p.send_msg(rep.clone().map(Into::into))).map_err(|e| log::error!("{e:?}")) nb::block!(p.send_msg(rep.clone().map(Into::into))).map_err(|e| log::error!("{e:?}"))
@ -145,10 +163,21 @@ fn main() {
let env = env::Env::try_read().unwrap(); let env = env::Env::try_read().unwrap();
let toad = Toad::try_new(env.api.addr, Config::default()).unwrap(); let toad = Toad::try_new(env.api.addr, Config::default()).unwrap();
let pg = PostgresImpl::try_new(|| {
let env::Postgres {host, port, pass, user, ..} = &env.postgres;
::postgres::Client::connect(&format!("host={host} port={port} dbname=dnim user={user} password={pass}"), ::postgres::NoTls)
}, env.postgres.pool_size).unwrap();
// SAFETY // SAFETY
// this is safe because the server worker cannot outlive the main thread // these are safe because the server worker cannot outlive the main thread
let toad_ref: &'static Toad = unsafe { core::mem::transmute(&toad) };
server_worker(toad_ref); let toad_ref: &'static Toad = unsafe { core::mem::transmute::<&Toad, &'static Toad>(&toad) };
let pg: &'static PostgresImpl<::postgres::Client> =
unsafe { core::mem::transmute::<&PostgresImpl<_>, &'static PostgresImpl<_>>(&pg) };
let app = AppConcrete { pg,
hashed_text_ext: HashedTextExtImpl(pg),
user: UserRepoImpl(pg) };
server_worker(&app, toad_ref);
} }

211
src/perm.rs Normal file
View File

@ -0,0 +1,211 @@
use std::str::FromStr;
use naan::prelude::*;
use crate::group::{Group, GroupId};
use crate::postgres::UnmarshalRow;
use crate::user::UserId;
#[derive(Default, Clone, PartialEq, Eq, Debug)]
pub struct Actor {
pub uid: Option<UserId>,
pub groups: Vec<Group>,
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub enum Dir<T, Id> {
Dir,
Within(Id, T),
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub enum PostPath {
Deleted,
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub enum UserPath {
Tag,
Email,
Deleted,
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub enum CommunityPath {
Posts(Dir<PostPath, ()>),
Tag,
Deleted,
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub enum GroupPath {
Members,
Deleted,
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub enum Path {
Community(Dir<CommunityPath, ()>),
User(Dir<UserPath, UserId>),
Group(Dir<GroupPath, GroupId>),
Unknown(String),
}
impl Path {
fn parse<S>(s: S) -> Path
where S: AsRef<str>
{
std::path::PathBuf::from_str(s.as_ref())
.map(|p| {
let mut seg = p.components();
macro_rules! next {
() => {{ seg.next().and_then(|p| p.as_os_str().to_str()) }}
}
if p.has_root() {
next!();
}
match next!() {
Some("communities") => match next!() {
Some(id) => match next!() {
Some("posts") => match next!() {
Some(id) => match next!() {
Some("deleted") => Some(Path::Community(Dir::Within((), CommunityPath::Posts(Dir::Within((), PostPath::Deleted))))),
Some(_) | None => None,
},
None => Some(Path::Community(Dir::Within((), CommunityPath::Posts(Dir::Dir))))
},
Some("tag") => Some(Path::Community(Dir::Within((), CommunityPath::Tag))),
Some("deleted") => Some(Path::Community(Dir::Within((), CommunityPath::Deleted))),
Some(_) | None => None,
},
None => Some(Path::Community(Dir::Dir)),
},
Some("users") => match next!() {
Some(id) => match next!() {
Some("deleted") => Some(Path::User(Dir::Within(UserId::from(id), UserPath::Deleted))),
Some("tag") => Some(Path::User(Dir::Within(UserId::from(id), UserPath::Tag))),
Some("email") => Some(Path::User(Dir::Within(UserId::from(id), UserPath::Email))),
_ => None,
},
None => Some(Path::User(Dir::Dir)),
},
Some("groups") => match next!() {
Some(id) => match next!() {
Some("members") => Some(Path::Group(Dir::Within(GroupId::from(id), GroupPath::Members))),
Some("deleted") => Some(Path::Group(Dir::Within(GroupId::from(id), GroupPath::Deleted))),
_ => None,
},
None => Some(Path::Group(Dir::Dir)),
},
_ => None,
}
})
.map(|o| o.unwrap_or_else(|| Path::Unknown(s.as_ref().to_string())))
.unwrap_or_else(|_| Path::Unknown(s.as_ref().to_string()))
}
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub enum Mode {
None,
Read,
Write,
}
impl Mode {
pub fn covered_by(&self, other: &Self) -> bool {
use Mode::*;
match self {
| None => true,
| Read => other == &Read || other == &Write,
| Write => other == &Write,
}
}
}
impl UnmarshalRow for Mode {
fn unmarshal_maybe_prefixed<R, S>(p: Option<S>, row: &R) -> Result<Self, R::Error>
where R: postgres::GenericRow,
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
}
})
}
}
pub struct Perm {
pub path: Path,
pub owner: (UserId, Mode),
pub group: (GroupId, Mode),
pub everyone: Mode,
}
impl UnmarshalRow for Perm {
fn unmarshal_maybe_prefixed<R, S>(p: Option<S>, row: &R) -> Result<Self, R::Error>
where R: postgres::GenericRow,
S: AsRef<str>
{
panic!("Perm cannot be prefixed")
}
fn unmarshal<R>(row: &R) -> Result<Self, R::Error>
where R: postgres::GenericRow
{
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))
.map(|(((((owner_mode, group_mode), everyone_mode), owner), group), path)| Perm {
path: Path::parse(path),
owner: (UserId::from(owner), owner_mode),
group: (GroupId::from(group), group_mode),
everyone: everyone_mode,
})
}
}
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)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn path_parse() {
assert_eq!(Path::parse("/communities"), Path::Community(Dir::Dir));
assert_eq!(Path::parse("/communities/1/posts/1/deleted"),
Path::Community(Dir::Within((),
CommunityPath::Posts(Dir::Within((),
PostPath::Deleted)))));
assert_eq!(Path::parse("/users/1/tag"),
Path::User(Dir::Within(UserId::from("1"), UserPath::Tag)));
assert_eq!(Path::parse("/users"), Path::User(Dir::Dir));
assert_eq!(Path::parse("/users/"), Path::User(Dir::Dir));
assert_eq!(Path::parse("/users/1"),
Path::Unknown(String::from("/users/1")));
assert_eq!(Path::parse("/groups"), Path::Group(Dir::Dir));
assert_eq!(Path::parse("/groups/1/members"),
Path::Group(Dir::Within(GroupId::from("1"), GroupPath::Members)));
}
}

View File

@ -2,14 +2,43 @@ use std::ops::DerefMut;
use std::pin::Pin; use std::pin::Pin;
use std::sync::{Mutex, MutexGuard}; use std::sync::{Mutex, MutexGuard};
use postgres::types::FromSql;
use postgres::GenericRow; use postgres::GenericRow;
use rand::Rng; use rand::Rng;
use crate::perm::Actor;
pub type DbError<Pg> = <<Pg as Postgres>::Client as postgres::GenericClient>::Error; pub type DbError<Pg> = <<Pg as Postgres>::Client as postgres::GenericClient>::Error;
pub trait UnmarshalRow: Sized + Send + Sync { 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())
}
fn unmarshal_maybe_prefixed<R, S>(col_prefix: Option<S>, row: &R) -> Result<Self, R::Error>
where R: GenericRow,
S: AsRef<str>;
fn unmarshal_prefixed<R, S>(col_prefix: S, row: &R) -> Result<Self, R::Error>
where R: GenericRow,
S: AsRef<str>
{
Self::unmarshal_maybe_prefixed(Some(col_prefix), row)
}
fn unmarshal<R>(row: &R) -> Result<Self, R::Error> fn unmarshal<R>(row: &R) -> Result<Self, R::Error>
where R: GenericRow; where R: GenericRow
{
Self::unmarshal_maybe_prefixed::<_, &'static str>(None, row)
}
} }
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] #[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
@ -30,6 +59,8 @@ pub trait Postgres
fn with_client<F, R>(&self, f: F) -> Result<R, DbError<Self>> fn with_client<F, R>(&self, f: F) -> Result<R, DbError<Self>>
where F: FnOnce(&mut Self::Client) -> Result<R, DbError<Self>>; where F: FnOnce(&mut Self::Client) -> Result<R, DbError<Self>>;
fn authorized(action: String, actor: &Actor) -> Result<(), DbError<Self>>;
} }
pub struct PostgresImpl<C> { pub struct PostgresImpl<C> {
@ -82,6 +113,10 @@ impl<C> Postgres for PostgresImpl<C>
| None => f(self.block_for_next().deref_mut()), | None => f(self.block_for_next().deref_mut()),
} }
} }
fn authorized(action: String, actor: &Actor) -> Result<(), DbError<Self>> {
todo!()
}
} }
#[cfg(test)] #[cfg(test)]

View File

@ -1,3 +1,5 @@
use crate::perm::Actor;
pub trait Repo: Send + Sync { pub trait Repo: Send + Sync {
type T; type T;
type Patch; type Patch;
@ -5,11 +7,11 @@ pub trait Repo: Send + Sync {
type Error: core::fmt::Debug; type Error: core::fmt::Debug;
type Id: AsRef<str>; type Id: AsRef<str>;
fn get(&self, id: Self::Id) -> Result<Option<Self::T>, Self::Error>; fn get(&self, actor: &Actor, id: &Self::Id) -> Result<Option<Self::T>, Self::Error>;
fn get_all(&self) -> Result<Vec<Self::T>, Self::Error>; fn get_all(&self, actor: &Actor) -> Result<Vec<Self::T>, Self::Error>;
fn patch(&self, id: Self::Id, state: Self::Patch) -> Result<bool, Self::Error>; fn patch(&self, actor: &Actor, id: &Self::Id, state: Self::Patch) -> Result<bool, Self::Error>;
fn insert(&self, state: Self::Insert) -> Result<Self::Id, Self::Error>; fn insert(&self, actor: &Actor, state: Self::Insert) -> Result<Self::Id, Self::Error>;
fn del(&self, id: Self::Id) -> Result<bool, Self::Error>; fn del(&self, actor: &Actor, id: &Self::Id) -> Result<bool, Self::Error>;
} }
/// An entity that has some operations which rely on /// An entity that has some operations which rely on

21
src/req_payload.rs Normal file
View File

@ -0,0 +1,21 @@
use serde::de::DeserializeOwned;
use toad_msg::alloc::Message;
use toad_msg::{ContentFormat, MessageOptions};
#[derive(serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct ReqPayload<T> {
#[serde(flatten)]
pub t: T,
pub session: String,
}
impl<T> ReqPayload<T> where T: DeserializeOwned
{
pub fn try_get(m: &Message) -> Result<Option<Self>, serde_json::Error> {
if m.payload().as_bytes().len() == 0 || m.content_format() != Some(ContentFormat::Json) {
Ok(None)
} else {
serde_json::from_slice::<Self>(m.payload().as_bytes()).map(Some)
}
}
}

View File

@ -2,6 +2,7 @@ use naan::prelude::*;
use postgres::{GenericClient, GenericRow}; use postgres::{GenericClient, GenericRow};
use crate::hashed_text::HashedText; use crate::hashed_text::HashedText;
use crate::perm::Actor;
use crate::postgres::{DbError, Postgres, UnmarshalRow}; use crate::postgres::{DbError, Postgres, UnmarshalRow};
use crate::repo::Repo; use crate::repo::Repo;
use crate::{newtype, Email}; use crate::{newtype, Email};
@ -25,21 +26,24 @@ pub struct User {
} }
impl UnmarshalRow for User { impl UnmarshalRow for User {
fn unmarshal<R>(row: &R) -> Result<Self, R::Error> fn unmarshal_maybe_prefixed<R, S>(p: Option<S>, row: &R) -> Result<Self, R::Error>
where R: GenericRow where R: GenericRow,
S: AsRef<str>
{ {
row.try_get::<_, String>("uid") let p = p.as_ref().map(|p| p.as_ref());
.zip(|_: &_| row.try_get::<_, String>("tag")) Self::try_get(p, "uid", row).zip(|_: &_| Self::try_get(p, "tag", row))
.zip(|_: &_| row.try_get::<_, String>("password")) .zip(|_: &_| Self::try_get::<String, _, _, _>(p, "password", row))
.zip(|_: &_| row.try_get::<_, String>("email")) .zip(|_: &_| Self::try_get(p, "email", row))
.map(|(((uid, tag), password), email)| User { uid: UserId(uid), .map(|(((uid, tag), password), email)| {
User { uid: UserId(uid),
tag: UserTag(tag), tag: UserTag(tag),
password: HashedText::from(password), password: HashedText::from(password),
email: Email(email) }) email: Email(email) }
})
} }
} }
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] #[derive(Default, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct UserPatch { pub struct UserPatch {
tag: Option<UserTag>, tag: Option<UserTag>,
password: Option<HashedText>, password: Option<HashedText>,
@ -55,7 +59,7 @@ pub struct UserInsert {
pub trait UserRepo: Repo<T = User, Patch = UserPatch, Id = UserId> {} pub trait UserRepo: Repo<T = User, Patch = UserPatch, Id = UserId> {}
pub struct UserRepoImpl<Db: 'static>(&'static Db); pub struct UserRepoImpl<Db: 'static>(pub &'static Db);
impl<Db> Repo for UserRepoImpl<Db> where Db: Postgres + 'static impl<Db> Repo for UserRepoImpl<Db> where Db: Postgres + 'static
{ {
type T = User; type T = User;
@ -64,9 +68,9 @@ impl<Db> Repo for UserRepoImpl<Db> where Db: Postgres + 'static
type Error = DbError<Db>; type Error = DbError<Db>;
type Id = UserId; type Id = UserId;
fn get(&self, id: Self::Id) -> Result<Option<Self::T>, Self::Error> { fn get(&self, actor: &Actor, id: &Self::Id) -> Result<Option<Self::T>, Self::Error> {
static QUERY: &'static str = static QUERY: &'static str =
"select uid, tag, password, email from public.usr where id = $1 :: uuid"; "select uid, tag, password, email from public.usr where id = $1 :: uuid and deleted = false";
self.0 self.0
.with_client(|c| c.query_opt(QUERY, &[&id.as_ref()])) .with_client(|c| c.query_opt(QUERY, &[&id.as_ref()]))
@ -77,20 +81,22 @@ impl<Db> Repo for UserRepoImpl<Db> where Db: Postgres + 'static
}) })
} }
fn get_all(&self) -> Result<Vec<Self::T>, Self::Error> { fn get_all(&self, actor: &Actor) -> Result<Vec<Self::T>, Self::Error> {
static QUERY: &'static str = "select uid, tag, password, email from public.usr"; static QUERY: &'static str =
"select uid, tag, password, email from public.usr where deleted = false";
self.0 self.0
.with_client(|c| c.query(QUERY, &[])) .with_client(|c| c.query(QUERY, &[]))
.and_then(|vec| vec.iter().map(User::unmarshal).collect()) .and_then(|vec| vec.iter().map(User::unmarshal).collect())
} }
fn patch(&self, id: UserId, patch: UserPatch) -> Result<bool, Self::Error> { fn patch(&self, actor: &Actor, id: &UserId, patch: UserPatch) -> Result<bool, Self::Error> {
static QUERY_LINES: [&'static str; 5] = ["update public.usr", static QUERY_LINES: [&'static str; 6] = ["update public.usr",
"set tag = coalesce($2, tag)", "set tag = coalesce($2, tag)",
" , password = coalesce($3, password)", " , password = coalesce($3, password)",
" , email = coalesce($4, email)", " , email = coalesce($4, email)",
"from public.usr where uid = $1"]; "from public.usr",
"where uid = $1 and deleted = false"];
self.0 self.0
.with_client(|c| { .with_client(|c| {
@ -104,7 +110,7 @@ impl<Db> Repo for UserRepoImpl<Db> where Db: Postgres + 'static
.map(|n| n == 1) .map(|n| n == 1)
} }
fn insert(&self, insert: UserInsert) -> Result<UserId, Self::Error> { fn insert(&self, actor: &Actor, insert: UserInsert) -> Result<UserId, Self::Error> {
static QUERY_LINES: [&'static str; 5] = ["insert into public.usr", static QUERY_LINES: [&'static str; 5] = ["insert into public.usr",
" (tag, password, email)", " (tag, password, email)",
"values", "values",
@ -123,7 +129,7 @@ impl<Db> Repo for UserRepoImpl<Db> where Db: Postgres + 'static
.map(UserId::from) .map(UserId::from)
} }
fn del(&self, id: UserId) -> Result<bool, Self::Error> { fn del(&self, actor: &Actor, id: &UserId) -> Result<bool, Self::Error> {
static QUERY: &'static str = "delete from public.usr where uid = $1 :: uuid;"; static QUERY: &'static str = "delete from public.usr where uid = $1 :: uuid;";
self.0 self.0
@ -140,10 +146,11 @@ mod tests {
use super::{User, UserRepoImpl}; use super::{User, UserRepoImpl};
use crate::hashed_text::HashedText; use crate::hashed_text::HashedText;
use crate::perm::Actor;
use crate::postgres::test::{from_sql_owned, Client, Row}; use crate::postgres::test::{from_sql_owned, Client, Row};
use crate::postgres::{Postgres, PostgresImpl}; use crate::postgres::{Postgres, PostgresImpl};
use crate::repo::Repo; use crate::repo::Repo;
use crate::user::{UserId, UserTag}; use crate::user::{UserId, UserInsert, UserTag};
use crate::Email; use crate::Email;
fn usr_row(usr: User) -> Row<()> { fn usr_row(usr: User) -> Row<()> {
@ -159,7 +166,6 @@ mod tests {
#[test] #[test]
fn user_repo_get_one() { fn user_repo_get_one() {
let client = || Client::<()> { query_opt: Box::new(|_, q, ps| { let client = || Client::<()> { query_opt: Box::new(|_, q, ps| {
assert_eq!(q.unwrap_str(), "select uid, tag, password, email from public.usr where id = $1 :: uuid");
Ok(Some(usr_row(User {uid: UserId::from("1"), tag: UserTag::from("foo"), email: Email::from("foo@bar.baz"), password: HashedText::from("XXX")})).filter(|_| from_sql_owned::<String>(ps[0]) == String::from("1"))) Ok(Some(usr_row(User {uid: UserId::from("1"), tag: UserTag::from("foo"), email: Email::from("foo@bar.baz"), password: HashedText::from("XXX")})).filter(|_| from_sql_owned::<String>(ps[0]) == String::from("1")))
}), }),
..Client::default() }; ..Client::default() };
@ -169,15 +175,17 @@ mod tests {
let repo = let repo =
UserRepoImpl(unsafe { std::mem::transmute::<_, &'static PostgresImpl<Client<()>>>(&db) }); UserRepoImpl(unsafe { std::mem::transmute::<_, &'static PostgresImpl<Client<()>>>(&db) });
assert!(repo.get(UserId::from("1")).unwrap().is_some()); assert!(repo.get(&Actor::default(), &UserId::from("1"))
assert!(repo.get(UserId::from("0")).unwrap().is_none()); .unwrap()
.is_some());
assert!(repo.get(&Actor::default(), &UserId::from("0"))
.unwrap()
.is_none());
} }
#[test] #[test]
fn user_repo_get_all() { fn user_repo_get_all() {
let client = || Client::<()> { query: Box::new(|_, q, _| { let client = || Client::<()> { query: Box::new(|_, q, _| {
assert_eq!(q.unwrap_str(),
"select uid, tag, password, email from public.usr");
Ok(vec![usr_row(User { uid: UserId::from("1"), Ok(vec![usr_row(User { uid: UserId::from("1"),
tag: UserTag::from("foo"), tag: UserTag::from("foo"),
email: Email::from("foo@bar.baz"), email: Email::from("foo@bar.baz"),
@ -190,7 +198,7 @@ mod tests {
let repo = let repo =
UserRepoImpl(unsafe { std::mem::transmute::<_, &'static PostgresImpl<Client<()>>>(&db) }); UserRepoImpl(unsafe { std::mem::transmute::<_, &'static PostgresImpl<Client<()>>>(&db) });
assert_eq!(repo.get_all().unwrap().len(), 1); assert_eq!(repo.get_all(&Actor::default()).unwrap().len(), 1);
} }
#[test] #[test]
@ -198,9 +206,6 @@ mod tests {
let client = let client =
|| Client::<()> { state: Box::new(false), // already deleted? || Client::<()> { state: Box::new(false), // already deleted?
execute: Box::new(|c, q, ps| { execute: Box::new(|c, q, ps| {
assert_eq!(q.unwrap_str(),
"delete from public.usr where uid = $1 :: uuid;");
if from_sql_owned::<String>(ps[0]) == "1" && !*c.state_mut::<bool>() { if from_sql_owned::<String>(ps[0]) == "1" && !*c.state_mut::<bool>() {
*c.state_mut::<bool>() = true; *c.state_mut::<bool>() = true;
Ok(1) Ok(1)
@ -215,26 +220,18 @@ mod tests {
let repo = let repo =
UserRepoImpl(unsafe { std::mem::transmute::<_, &'static PostgresImpl<Client<()>>>(&db) }); UserRepoImpl(unsafe { std::mem::transmute::<_, &'static PostgresImpl<Client<()>>>(&db) });
assert_eq!(repo.del(UserId::from("1")).unwrap(), true); assert_eq!(repo.del(&Actor::default(), &UserId::from("1")).unwrap(),
assert_eq!(repo.del(UserId::from("1")).unwrap(), false); true);
assert_eq!(repo.del(UserId::from("2")).unwrap(), false); assert_eq!(repo.del(&Actor::default(), &UserId::from("1")).unwrap(),
false);
assert_eq!(repo.del(&Actor::default(), &UserId::from("2")).unwrap(),
false);
} }
#[test] #[test]
fn user_repo_insert() { fn user_repo_insert() {
let client = || Client::<()> { state: Box::new(vec![UserTag::from("foo")]), let client = || Client::<()> { state: Box::new(Vec::<UserTag>::new()),
query_one: Box::new(|c, q, ps| { query_one: Box::new(|c, q, ps| {
assert_eq!(
q.unwrap_str(),
format!(
"insert into public.usr
(tag, password, email)
values
($2, $3, $4)
returning uid;"
)
);
let tags = c.state_mut::<Vec<UserTag>>(); 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]));
@ -255,8 +252,20 @@ returning uid;"
let repo = let repo =
UserRepoImpl(unsafe { std::mem::transmute::<_, &'static PostgresImpl<Client<()>>>(&db) }); UserRepoImpl(unsafe { std::mem::transmute::<_, &'static PostgresImpl<Client<()>>>(&db) });
assert!(repo.insert(UserId::from("1")).is_ok()); assert!(repo.insert(&Actor::default(),
assert!(repo.insert(UserId::from("1")).is_err()); UserInsert { tag: UserTag::from("foo"),
assert!(repo.insert(UserId::from("2")).is_ok()); password: HashedText::from("poop"),
email: Email::from("foo@bar.baz") })
.is_ok());
assert!(repo.insert(&Actor::default(),
UserInsert { tag: UserTag::from("foo"),
password: HashedText::from("poop"),
email: Email::from("foo@bar.baz") })
.is_err());
assert!(repo.insert(&Actor::default(),
UserInsert { tag: UserTag::from("bar"),
password: HashedText::from("poop"),
email: Email::from("bar@bar.baz") })
.is_ok());
} }
} }

66
src/user_session.rs Normal file
View File

@ -0,0 +1,66 @@
use std::net::SocketAddr;
use naan::prelude::*;
use postgres::GenericClient;
use crate::newtype;
use crate::postgres::{DbError, Postgres, UnmarshalRow};
use crate::repo::Ext;
use crate::user::UserId;
newtype!(
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct UserSession(String);
);
pub enum ClaimsError<E> {
Invalid,
Expired,
Other(E),
}
pub enum LoginError<E> {
NoSuchUser,
Other(E),
}
pub trait UserSessionExt: Ext {
fn login<TE, P, L, D>(&self,
tag_or_email: TE,
password: P,
remember: bool,
location: L,
device: D,
ip: SocketAddr)
-> Result<UserSession, LoginError<Self::Error>>
where TE: AsRef<str>,
P: AsRef<str>,
L: AsRef<str>,
D: AsRef<str>;
}
pub struct UserSessionExtImpl<Db: 'static>(&'static Db);
impl<Db> Ext for UserSessionExtImpl<Db> where Db: Postgres
{
type Error = DbError<Db>;
}
impl<Db> UserSessionExt for UserSessionExtImpl<Db> where Db: Postgres
{
fn login<TE, P, L, D>(&self,
tag_or_email: TE,
password: P,
remember: bool,
location: L,
device: D,
ip: SocketAddr)
-> Result<UserSession, LoginError<DbError<Db>>>
where TE: AsRef<str>,
P: AsRef<str>,
L: AsRef<str>,
D: AsRef<str>
{
todo!()
}
}