feat: add permissions model
This commit is contained in:
parent
81f811f8d3
commit
3722a9ce2f
34
src/app.rs
34
src/app.rs
@ -1,13 +1,37 @@
|
||||
use crate::hashed_text::{HashedTextExt, HashedTextExtImpl};
|
||||
use crate::postgres::PostgresImpl;
|
||||
use crate::user::UserRepo;
|
||||
use crate::postgres::{Postgres, PostgresImpl};
|
||||
use crate::user::{UserRepo, UserRepoImpl};
|
||||
|
||||
trait App: Send + Sync + Sized {
|
||||
pub trait App: Send + Sync + Sized {
|
||||
type Db: Postgres;
|
||||
type HashedTextExt: HashedTextExt;
|
||||
type UserRepo: UserRepo;
|
||||
|
||||
fn db(&self) -> &Self::Db;
|
||||
fn hashed_text(&self) -> &Self::HashedTextExt;
|
||||
fn user(&self) -> &Self::UserRepo;
|
||||
}
|
||||
|
||||
pub struct AppConcrete {
|
||||
pg: PostgresImpl<postgres::Client>,
|
||||
hashed_text_ext: HashedTextExtImpl<PostgresImpl<postgres::Client>>,
|
||||
pub pg: &'static 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
|
||||
}
|
||||
}
|
||||
|
18
src/env.rs
18
src/env.rs
@ -1,5 +1,6 @@
|
||||
use std::ffi::OsString;
|
||||
use std::net::{AddrParseError, SocketAddr};
|
||||
use std::num::ParseIntError;
|
||||
use std::str::FromStr;
|
||||
|
||||
use naan::prelude::*;
|
||||
@ -15,6 +16,7 @@ pub struct Postgres {
|
||||
pub pass: String,
|
||||
pub host: String,
|
||||
pub port: String,
|
||||
pub pool_size: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
@ -26,7 +28,8 @@ pub struct Env {
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Error {
|
||||
VarNotUnicode(String, OsString),
|
||||
VarNotSocketAddr(String, AddrParseError),
|
||||
VarNotSocketAddr(String, String, AddrParseError),
|
||||
VarNotInt(String, String, ParseIntError),
|
||||
RequiredVarNotSet(String),
|
||||
}
|
||||
|
||||
@ -48,13 +51,20 @@ impl Env {
|
||||
let postgres = Postgres { user: get_required("POSTGRES_USER")?,
|
||||
host: get_required("POSTGRES_HOST")?,
|
||||
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 = Api { addr: api_addr.trim()
|
||||
let api =
|
||||
Api { addr: api_addr.trim()
|
||||
.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 })
|
||||
}
|
||||
|
93
src/group.rs
Normal file
93
src/group.rs
Normal 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!()
|
||||
}
|
||||
}
|
@ -13,7 +13,7 @@ pub trait HashedTextExt: Ext {
|
||||
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
|
||||
{
|
||||
|
43
src/main.rs
43
src/main.rs
@ -1,3 +1,6 @@
|
||||
use app::{App, AppConcrete};
|
||||
use hashed_text::{HashedTextExt, HashedTextExtImpl};
|
||||
use repo::Repo;
|
||||
use toad::config::Config;
|
||||
use toad::net::Addrd;
|
||||
use toad::platform::Platform as _;
|
||||
@ -6,12 +9,18 @@ use toad::resp::Resp;
|
||||
use toad_msg::alloc::Message;
|
||||
use toad_msg::Type;
|
||||
|
||||
use crate::postgres::{Postgres, PostgresImpl};
|
||||
|
||||
mod app;
|
||||
mod env;
|
||||
mod group;
|
||||
mod hashed_text;
|
||||
mod perm;
|
||||
mod postgres;
|
||||
mod repo;
|
||||
mod req_payload;
|
||||
mod user;
|
||||
mod user_session;
|
||||
mod uuid;
|
||||
|
||||
#[macro_export]
|
||||
@ -100,8 +109,11 @@ mod __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()
|
||||
.path()
|
||||
.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") {
|
||||
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 =
|
||||
Message::builder(Type::Ack, toad::resp::code::NOT_FOUND).token(req.data().msg().token)
|
||||
.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 {
|
||||
match nb::block!(p.poll_req()) {
|
||||
| Err(e) => log::error!("{e:?}"),
|
||||
| Ok(req) => match handle_request(req) {
|
||||
| Ok(req) => match handle_request(app, req) {
|
||||
| Err(e) => log::error!("{e:?}"),
|
||||
| Ok(rep) => {
|
||||
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 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
|
||||
// this is safe because the server worker cannot outlive the main thread
|
||||
let toad_ref: &'static Toad = unsafe { core::mem::transmute(&toad) };
|
||||
// these are safe because the server worker cannot outlive the main thread
|
||||
|
||||
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
211
src/perm.rs
Normal 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)));
|
||||
}
|
||||
}
|
@ -2,14 +2,43 @@ use std::ops::DerefMut;
|
||||
use std::pin::Pin;
|
||||
use std::sync::{Mutex, MutexGuard};
|
||||
|
||||
use postgres::types::FromSql;
|
||||
use postgres::GenericRow;
|
||||
use rand::Rng;
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
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>
|
||||
where R: GenericRow;
|
||||
where R: GenericRow
|
||||
{
|
||||
Self::unmarshal_maybe_prefixed::<_, &'static str>(None, row)
|
||||
}
|
||||
}
|
||||
|
||||
#[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>>
|
||||
where F: FnOnce(&mut Self::Client) -> Result<R, DbError<Self>>;
|
||||
|
||||
fn authorized(action: String, actor: &Actor) -> Result<(), DbError<Self>>;
|
||||
}
|
||||
|
||||
pub struct PostgresImpl<C> {
|
||||
@ -82,6 +113,10 @@ impl<C> Postgres for PostgresImpl<C>
|
||||
| None => f(self.block_for_next().deref_mut()),
|
||||
}
|
||||
}
|
||||
|
||||
fn authorized(action: String, actor: &Actor) -> Result<(), DbError<Self>> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
12
src/repo.rs
12
src/repo.rs
@ -1,3 +1,5 @@
|
||||
use crate::perm::Actor;
|
||||
|
||||
pub trait Repo: Send + Sync {
|
||||
type T;
|
||||
type Patch;
|
||||
@ -5,11 +7,11 @@ pub trait Repo: Send + Sync {
|
||||
type Error: core::fmt::Debug;
|
||||
type Id: AsRef<str>;
|
||||
|
||||
fn get(&self, id: Self::Id) -> Result<Option<Self::T>, Self::Error>;
|
||||
fn get_all(&self) -> Result<Vec<Self::T>, Self::Error>;
|
||||
fn patch(&self, id: Self::Id, state: Self::Patch) -> Result<bool, Self::Error>;
|
||||
fn insert(&self, state: Self::Insert) -> Result<Self::Id, Self::Error>;
|
||||
fn del(&self, id: Self::Id) -> Result<bool, Self::Error>;
|
||||
fn get(&self, actor: &Actor, id: &Self::Id) -> Result<Option<Self::T>, Self::Error>;
|
||||
fn get_all(&self, actor: &Actor) -> Result<Vec<Self::T>, Self::Error>;
|
||||
fn patch(&self, actor: &Actor, id: &Self::Id, state: Self::Patch) -> Result<bool, Self::Error>;
|
||||
fn insert(&self, actor: &Actor, state: Self::Insert) -> Result<Self::Id, Self::Error>;
|
||||
fn del(&self, actor: &Actor, id: &Self::Id) -> Result<bool, Self::Error>;
|
||||
}
|
||||
|
||||
/// An entity that has some operations which rely on
|
||||
|
21
src/req_payload.rs
Normal file
21
src/req_payload.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
103
src/user.rs
103
src/user.rs
@ -2,6 +2,7 @@ use naan::prelude::*;
|
||||
use postgres::{GenericClient, GenericRow};
|
||||
|
||||
use crate::hashed_text::HashedText;
|
||||
use crate::perm::Actor;
|
||||
use crate::postgres::{DbError, Postgres, UnmarshalRow};
|
||||
use crate::repo::Repo;
|
||||
use crate::{newtype, Email};
|
||||
@ -25,21 +26,24 @@ pub struct User {
|
||||
}
|
||||
|
||||
impl UnmarshalRow for User {
|
||||
fn unmarshal<R>(row: &R) -> Result<Self, R::Error>
|
||||
where R: GenericRow
|
||||
fn unmarshal_maybe_prefixed<R, S>(p: Option<S>, row: &R) -> Result<Self, R::Error>
|
||||
where R: GenericRow,
|
||||
S: AsRef<str>
|
||||
{
|
||||
row.try_get::<_, String>("uid")
|
||||
.zip(|_: &_| row.try_get::<_, String>("tag"))
|
||||
.zip(|_: &_| row.try_get::<_, String>("password"))
|
||||
.zip(|_: &_| row.try_get::<_, String>("email"))
|
||||
.map(|(((uid, tag), password), email)| User { uid: UserId(uid),
|
||||
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) })
|
||||
email: Email(email) }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||
#[derive(Default, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||
pub struct UserPatch {
|
||||
tag: Option<UserTag>,
|
||||
password: Option<HashedText>,
|
||||
@ -55,7 +59,7 @@ pub struct UserInsert {
|
||||
|
||||
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
|
||||
{
|
||||
type T = User;
|
||||
@ -64,9 +68,9 @@ impl<Db> Repo for UserRepoImpl<Db> where Db: Postgres + 'static
|
||||
type Error = DbError<Db>;
|
||||
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 =
|
||||
"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
|
||||
.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> {
|
||||
static QUERY: &'static str = "select uid, tag, password, email from public.usr";
|
||||
fn get_all(&self, actor: &Actor) -> Result<Vec<Self::T>, Self::Error> {
|
||||
static QUERY: &'static str =
|
||||
"select uid, tag, password, email from public.usr where deleted = false";
|
||||
|
||||
self.0
|
||||
.with_client(|c| c.query(QUERY, &[]))
|
||||
.and_then(|vec| vec.iter().map(User::unmarshal).collect())
|
||||
}
|
||||
|
||||
fn patch(&self, id: UserId, patch: UserPatch) -> Result<bool, Self::Error> {
|
||||
static QUERY_LINES: [&'static str; 5] = ["update public.usr",
|
||||
fn patch(&self, actor: &Actor, id: &UserId, patch: UserPatch) -> Result<bool, Self::Error> {
|
||||
static QUERY_LINES: [&'static str; 6] = ["update public.usr",
|
||||
"set tag = coalesce($2, tag)",
|
||||
" , password = coalesce($3, password)",
|
||||
" , email = coalesce($4, email)",
|
||||
"from public.usr where uid = $1"];
|
||||
"from public.usr",
|
||||
"where uid = $1 and deleted = false"];
|
||||
|
||||
self.0
|
||||
.with_client(|c| {
|
||||
@ -104,7 +110,7 @@ impl<Db> Repo for UserRepoImpl<Db> where Db: Postgres + 'static
|
||||
.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",
|
||||
" (tag, password, email)",
|
||||
"values",
|
||||
@ -123,7 +129,7 @@ impl<Db> Repo for UserRepoImpl<Db> where Db: Postgres + 'static
|
||||
.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;";
|
||||
|
||||
self.0
|
||||
@ -140,10 +146,11 @@ mod tests {
|
||||
|
||||
use super::{User, UserRepoImpl};
|
||||
use crate::hashed_text::HashedText;
|
||||
use crate::perm::Actor;
|
||||
use crate::postgres::test::{from_sql_owned, Client, Row};
|
||||
use crate::postgres::{Postgres, PostgresImpl};
|
||||
use crate::repo::Repo;
|
||||
use crate::user::{UserId, UserTag};
|
||||
use crate::user::{UserId, UserInsert, UserTag};
|
||||
use crate::Email;
|
||||
|
||||
fn usr_row(usr: User) -> Row<()> {
|
||||
@ -159,7 +166,6 @@ mod tests {
|
||||
#[test]
|
||||
fn user_repo_get_one() {
|
||||
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")))
|
||||
}),
|
||||
..Client::default() };
|
||||
@ -169,15 +175,17 @@ mod tests {
|
||||
let repo =
|
||||
UserRepoImpl(unsafe { std::mem::transmute::<_, &'static PostgresImpl<Client<()>>>(&db) });
|
||||
|
||||
assert!(repo.get(UserId::from("1")).unwrap().is_some());
|
||||
assert!(repo.get(UserId::from("0")).unwrap().is_none());
|
||||
assert!(repo.get(&Actor::default(), &UserId::from("1"))
|
||||
.unwrap()
|
||||
.is_some());
|
||||
assert!(repo.get(&Actor::default(), &UserId::from("0"))
|
||||
.unwrap()
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_repo_get_all() {
|
||||
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"),
|
||||
tag: UserTag::from("foo"),
|
||||
email: Email::from("foo@bar.baz"),
|
||||
@ -190,7 +198,7 @@ mod tests {
|
||||
let repo =
|
||||
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]
|
||||
@ -198,9 +206,6 @@ mod tests {
|
||||
let client =
|
||||
|| Client::<()> { state: Box::new(false), // already deleted?
|
||||
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>() {
|
||||
*c.state_mut::<bool>() = true;
|
||||
Ok(1)
|
||||
@ -215,26 +220,18 @@ mod tests {
|
||||
let repo =
|
||||
UserRepoImpl(unsafe { std::mem::transmute::<_, &'static PostgresImpl<Client<()>>>(&db) });
|
||||
|
||||
assert_eq!(repo.del(UserId::from("1")).unwrap(), true);
|
||||
assert_eq!(repo.del(UserId::from("1")).unwrap(), false);
|
||||
assert_eq!(repo.del(UserId::from("2")).unwrap(), false);
|
||||
assert_eq!(repo.del(&Actor::default(), &UserId::from("1")).unwrap(),
|
||||
true);
|
||||
assert_eq!(repo.del(&Actor::default(), &UserId::from("1")).unwrap(),
|
||||
false);
|
||||
assert_eq!(repo.del(&Actor::default(), &UserId::from("2")).unwrap(),
|
||||
false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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| {
|
||||
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 tag = UserTag::from(from_sql_owned::<String>(ps[0]));
|
||||
@ -255,8 +252,20 @@ returning uid;"
|
||||
let repo =
|
||||
UserRepoImpl(unsafe { std::mem::transmute::<_, &'static PostgresImpl<Client<()>>>(&db) });
|
||||
|
||||
assert!(repo.insert(UserId::from("1")).is_ok());
|
||||
assert!(repo.insert(UserId::from("1")).is_err());
|
||||
assert!(repo.insert(UserId::from("2")).is_ok());
|
||||
assert!(repo.insert(&Actor::default(),
|
||||
UserInsert { tag: UserTag::from("foo"),
|
||||
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
66
src/user_session.rs
Normal 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!()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user