feat: lots of cool stuff!

i forgot to commmit
This commit is contained in:
Orion Kindel 2023-07-16 03:07:25 -04:00
parent 50f416d402
commit 46eaa49586
Signed by untrusted user who does not match committer: orion
GPG Key ID: 6D4165AE4C928719
20 changed files with 1301 additions and 687 deletions

7
Cargo.lock generated
View File

@ -40,6 +40,7 @@ dependencies = [
"simple_logger",
"toad",
"toad-msg",
"uuid",
]
[[package]]
@ -1140,6 +1141,12 @@ dependencies = [
"tinyvec",
]
[[package]]
name = "uuid"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d023da39d1fde5a8a3fe1f3e01ca9632ada0a63e9797de55a879d6e2236277be"
[[package]]
name = "vcpkg"
version = "0.2.15"

View File

@ -15,3 +15,4 @@ log = "0.4"
postgres = {path = "./postgres/postgres"}
rand = "0.8"
naan = "0.1.32"
uuid = "1.4"

View File

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

View File

@ -1,32 +1,39 @@
use std::ptr::hash;
use app::{App, AppConcrete};
use hashed_text::{HashedTextExt, HashedTextExtImpl};
use repo::Repo;
use model::{HashedTextExt,
HashedTextExtImpl,
UserId,
UserRepoImpl,
UserSession,
UserSessionExt,
UserSessionExtImpl};
use naan::prelude::*;
use repo::{Page, ReadMany, ReadOne, Repo};
use req_payload::ReqPayload;
use toad::config::Config;
use toad::net::Addrd;
use toad::platform::Platform as _;
use toad::req::Req;
use toad::resp::Resp;
use toad::resp::{code, Resp};
use toad_msg::alloc::Message;
use toad_msg::Type;
use toad_msg::{Code, MessageBuilder, Type};
use crate::model::GroupRepoImpl;
use crate::postgres::{Postgres, PostgresImpl};
mod app;
mod env;
mod group;
mod hashed_text;
mod perm;
mod model;
mod postgres;
mod rep_payload;
mod repo;
mod req_payload;
mod user;
mod user_session;
mod uuid;
#[macro_export]
macro_rules! newtype {
(
$(#[derive($($der:ident),+)])?
$(#[derive($($der:path),+)])?
$(#[doc = $doc:literal])*
pub struct $name:ident(String);
) => {
@ -40,9 +47,9 @@ macro_rules! newtype {
}
}
impl ToString for $name {
fn to_string(&self) -> String {
self.0.clone()
impl core::fmt::Display for $name {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}", self.0)
}
}
@ -63,6 +70,17 @@ macro_rules! newtype {
s.0
}
}
impl<'a> ::postgres::types::FromSql<'a> for $name {
fn from_sql(ty: &::postgres::types::Type, raw: &'a [u8]) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
let s = <&str as ::postgres::types::FromSql>::from_sql(ty, raw)?;
Ok($name::from(s))
}
fn accepts(ty: &::postgres::types::Type) -> bool {
<&str as ::postgres::types::FromSql>::accepts(ty)
}
}
};
(pub struct $name:ident($inner:ty);) => {
pub struct $name($inner);
@ -82,7 +100,7 @@ macro_rules! newtype {
}
newtype!(
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, serde::Serialize, serde::Deserialize)]
pub struct Email(String);
);
@ -109,35 +127,66 @@ mod __toad_aliases {
}
use __toad_aliases::*;
use user::{UserId, UserRepoImpl};
fn handle_request<A>(app: &A, req: Addrd<Req<ToadT>>) -> Result<Addrd<Message>, String>
fn handle_request<A>(app: &A, req: Addrd<Req<ToadT>>) -> Addrd<Message>
where A: App
{
let path = req.data()
.path()
.map_err(|e| format!("{e:?}"))
.unwrap_or(None)
.unwrap_or("");
let mut path_segments = path.split('/').peekable();
let body = || {
let mut msg =
Message::builder(Type::Ack, toad::resp::code::NOT_FOUND).token(req.data().msg().token);
if path_segments.peek() == Some(&"users") {
let mut path_segments = path_segments.clone();
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();
Ok(Addrd(msg, req.addr()))
} else {
let msg =
Message::builder(Type::Ack, toad::resp::code::NOT_FOUND).token(req.data().msg().token)
.build();
Ok(Addrd(msg, req.addr()))
}
let path = req.data().path().unwrap_or(None).unwrap_or("");
let mut path_segments = path.split('/').peekable();
let actor = Ok(req.data().payload()).and_then(serde_json::from_slice::<Option<ReqPayload<()>>>)
.map_err(|e| Box::new(e) as _)?
.and_then(|r| r.session)
.map(|s| app.user_session().touch(UserSession::from(s)))
.sequence::<hkt::ResultOk<_>>()
.map_err(|e| Box::new(e) as _)?
.unwrap_or_default();
if path_segments.peek() == Some(&"users") {
let mut path_segments = path_segments.clone();
if req.data().msg().code == Code::GET {
let id = path_segments.nth(2).map(UserId::from);
match id {
| Some(id) => {
let user = app.user().get(&actor, &id).map_err(|e| Box::new(e) as _)?;
msg = msg.payload(serde_json::to_vec(&user).map_err(|e| Box::new(e) as _)?);
},
| None => {
let payload =
Ok(req.data().payload()).and_then(serde_json::from_slice::<Option<ReqPayload<_>>>)
.map_err(|e| Box::new(e) as _)?
.ok_or_else(|| {
String::from("\"limit\" and \"after\" must be provided")
})
.map_err(|e| Box::new(e) as _)?;
let users = app.user()
.get_all(&actor, payload.t)
.map_err(|e| Box::new(e) as _)?;
let bytes = serde_json::to_vec(&users).map_err(|e| Box::new(e) as _)?;
msg = msg.payload(bytes);
},
}
}
} else {
msg = msg.code(toad::resp::code::NOT_FOUND);
}
Ok(Addrd(msg.build(), req.addr()))
};
body().map_err(|e: Box<dyn core::fmt::Debug>| log::error!("{e:?}"))
.unwrap_or_else(|_| {
Addrd(Message::builder(Type::Ack, code::INTERNAL_SERVER_ERROR).build(),
req.addr())
})
}
fn server_worker<A>(app: &A, p: &'static Toad)
@ -146,38 +195,57 @@ fn server_worker<A>(app: &A, p: &'static Toad)
loop {
match nb::block!(p.poll_req()) {
| Err(e) => log::error!("{e:?}"),
| 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:?}"))
.ok();
},
| Ok(req) => {
let rep = handle_request(app, req);
nb::block!(p.send_msg(rep.clone().map(Into::into))).map_err(|e| log::error!("{e:?}"))
.ok();
},
}
}
}
fn main() {
fn assert_static<'a, T>(t: &'a T) -> &'static T {
// SAFETY
// this will always be safe because the referenced data cannot outlive the main thread
unsafe { core::mem::transmute::<&T, &'static T>(t) }
}
simple_logger::init().unwrap();
let env = env::Env::try_read().unwrap();
let toad = Toad::try_new(env.api.addr, Config::default()).unwrap();
let toad = &toad;
let toad = assert_static(toad);
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();
let pg = &pg;
let pg = assert_static(pg);
// SAFETY
// these are safe because the server worker cannot outlive the main thread
let group_repo = GroupRepoImpl(pg);
let group_repo = &group_repo;
let group_repo = assert_static(group_repo);
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 hashed_text = HashedTextExtImpl(pg);
let hashed_text = &hashed_text;
let hashed_text = assert_static(hashed_text);
let user_session = UserSessionExtImpl(pg, group_repo);
let user_session = &user_session;
let user_session = assert_static(user_session);
let user = UserRepoImpl(pg);
let user = &user;
let user = assert_static(user);
let app = AppConcrete { pg,
hashed_text_ext: HashedTextExtImpl(pg),
user: UserRepoImpl(pg) };
hashed_text,
user_session,
user };
server_worker(&app, toad_ref);
server_worker(&app, toad);
}

6
src/model/community.rs Normal file
View File

@ -0,0 +1,6 @@
use crate::newtype;
newtype!(
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct CommunityId(String);
);

View File

@ -1,11 +1,10 @@
use naan::prelude::*;
use postgres::GenericClient;
use crate::model::{Actor, User, UserId};
use crate::newtype;
use crate::perm::Actor;
use crate::postgres::{try_get, DbError, Postgres, UnmarshalRow};
use crate::repo::Repo;
use crate::user::{User, UserId};
use crate::repo::{Del, Insert, Page, Patch, ReadMany, ReadOne, Repo};
newtype!(
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
@ -46,19 +45,40 @@ pub struct GroupInsert {
members: Vec<UserId>,
}
pub trait GroupRepo: Repo<T = Group, Patch = GroupPatch, Id = GroupId> {
pub trait GroupRepo:
Repo<T = Group, Id = GroupId>
+ ReadOne
+ Patch<Patch = GroupPatch>
+ Insert<Insert = GroupInsert>
+ Del
+ ReadMany
{
fn members(&self, id: &GroupId) -> Result<Vec<UserId>, Self::Error>;
fn groups(&self, id: &UserId) -> Result<Vec<GroupId>, Self::Error>;
}
pub struct GroupRepoImpl<Db: 'static>(pub &'static Db);
impl<Db> GroupRepo for GroupRepoImpl<Db> where Db: Postgres + 'static
{
fn members(&self, id: &GroupId) -> Result<Vec<UserId>, Self::Error> {
todo!()
}
fn groups(&self, id: &UserId) -> Result<Vec<GroupId>, Self::Error> {
todo!()
}
}
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;
}
impl<Db> ReadOne for GroupRepoImpl<Db> where Db: Postgres + 'static
{
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";
@ -70,8 +90,11 @@ impl<Db> Repo for GroupRepoImpl<Db> where Db: Postgres + 'static
.sequence::<hkt::ResultOk<_>>()
})
}
}
fn get_all(&self, actor: &Actor) -> Result<Vec<Group>, Self::Error> {
impl<Db> ReadMany for GroupRepoImpl<Db> where Db: Postgres + 'static
{
fn get_all(&self, actor: &Actor, page: Page<GroupId>) -> Result<Vec<Group>, Self::Error> {
static QUERY: &'static str = "select uid, tag, password, email from public.grp";
self.0
@ -79,14 +102,31 @@ impl<Db> Repo for GroupRepoImpl<Db> where Db: Postgres + 'static
.and_then(|vec| vec.iter().map(Group::unmarshal).collect())
}
fn page_valid(&self, page: &Page<Self::Id>) -> bool {
page.limit < 100
}
}
impl<Db> Patch for GroupRepoImpl<Db> where Db: Postgres + 'static
{
type Patch = GroupPatch;
fn patch(&self, actor: &Actor, id: &GroupId, state: GroupPatch) -> Result<bool, Self::Error> {
todo!()
}
}
impl<Db> Insert for GroupRepoImpl<Db> where Db: Postgres + 'static
{
type Insert = GroupInsert;
fn insert(&self, actor: &Actor, state: GroupInsert) -> Result<GroupId, Self::Error> {
todo!()
}
}
impl<Db> Del for GroupRepoImpl<Db> where Db: Postgres + 'static
{
fn del(&self, actor: &Actor, id: &GroupId) -> Result<bool, Self::Error> {
todo!()
}

14
src/model/mod.rs Normal file
View File

@ -0,0 +1,14 @@
pub mod community;
pub mod group;
pub mod hashed_text;
pub mod perm;
pub mod thread;
pub mod user;
pub mod user_session;
pub use community::*;
pub use group::*;
pub use hashed_text::*;
pub use perm::*;
pub use thread::*;
pub use user::*;
pub use user_session::*;

330
src/model/perm.rs Normal file
View File

@ -0,0 +1,330 @@
use std::convert::Infallible;
use std::str::FromStr;
use naan::prelude::*;
use postgres::types::{FromSql, Type};
use super::community::CommunityId;
use super::group::{Group, GroupId};
use super::thread::ThreadId;
use super::user::UserId;
use crate::postgres::{try_get, UnmarshalRow};
#[derive(Default, Clone, PartialEq, Eq, Debug)]
pub struct Actor {
pub uid: Option<UserId>,
pub groups: Vec<GroupId>,
}
#[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 ThreadsPath {
Deleted,
}
impl core::fmt::Display for ThreadsPath {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
| ThreadsPath::Deleted => write!(f, "deleted"),
}
}
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub enum UsersPath {
Tag,
Email,
Deleted,
Password,
}
impl core::fmt::Display for UsersPath {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
| UsersPath::Tag => write!(f, "tag"),
| UsersPath::Email => write!(f, "email"),
| UsersPath::Deleted => write!(f, "deleted"),
| UsersPath::Password => write!(f, "password"),
}
}
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub enum CommunitiesPath {
Threads(Dir<ThreadsPath, ThreadId>),
Tag,
Deleted,
}
impl core::fmt::Display for CommunitiesPath {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
| CommunitiesPath::Threads(Dir::Dir) => write!(f, "threads/"),
| CommunitiesPath::Threads(Dir::Within(id, p)) => write!(f, "threads/{id}/{p}"),
| CommunitiesPath::Tag => write!(f, "tag"),
| CommunitiesPath::Deleted => write!(f, "deleted"),
}
}
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub enum GroupsPath {
Members,
Deleted,
}
impl core::fmt::Display for GroupsPath {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
| GroupsPath::Members => write!(f, "members"),
| GroupsPath::Deleted => write!(f, "deleted"),
}
}
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub enum Path {
Community(Dir<CommunitiesPath, CommunityId>),
User(Dir<UsersPath, UserId>),
Group(Dir<GroupsPath, GroupId>),
Unknown(String),
}
impl core::fmt::Display for Path {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
| Path::Community(Dir::Dir) => write!(f, "/communities/"),
| Path::User(Dir::Dir) => write!(f, "/users/"),
| Path::Group(Dir::Dir) => write!(f, "/groups/"),
| Path::Community(Dir::Within(id, p)) => write!(f, "/communities/{id}/{p}"),
| Path::User(Dir::Within(id, p)) => write!(f, "/users/{id}/{p}"),
| Path::Group(Dir::Within(id, p)) => write!(f, "/groups/{id}/{p}"),
| Path::Unknown(s) => write!(f, "{s}"),
}
}
}
impl Path {
pub 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(cid) => {
let comm = |p| Path::Community(Dir::Within(CommunityId::from(cid), p));
match next!() {
| Some("threads") => match next!() {
| Some(tid) => {
let thread =
|p| comm(CommunitiesPath::Threads(Dir::Within(ThreadId::from(tid), p)));
match next!() {
| Some("deleted") => Some(thread(ThreadsPath::Deleted)),
| _ => None,
}
},
| None => Some(comm(CommunitiesPath::Threads(Dir::Dir))),
},
| Some("tag") => Some(comm(CommunitiesPath::Tag)),
| Some("deleted") => Some(comm(CommunitiesPath::Deleted)),
| _ => None,
}
},
| None => Some(Path::Community(Dir::Dir)),
},
| Some("users") => match next!() {
| Some(id) => {
let usr = |p| Path::User(Dir::Within(UserId::from(id), p));
match next!() {
| Some("deleted") => Some(usr(UsersPath::Deleted)),
| Some("tag") => Some(usr(UsersPath::Tag)),
| Some("email") => Some(usr(UsersPath::Email)),
| Some("password") => Some(usr(UsersPath::Password)),
| _ => None,
}
},
| None => Some(Path::User(Dir::Dir)),
},
| Some("groups") => match next!() {
| Some(id) => {
let grp = |p| Path::Group(Dir::Within(GroupId::from(id), p));
match next!() {
| Some("members") => Some(grp(GroupsPath::Members)),
| Some("deleted") => Some(grp(GroupsPath::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 FromStr for Mode {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
| "r" => Mode::Read,
| "w" => Mode::Write,
| _ => Mode::None,
})
}
}
impl<'a> FromSql<'a> for Mode {
fn from_sql(ty: &Type, raw: &'a [u8]) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
let s = <&str as FromSql>::from_sql(ty, raw)?;
Mode::from_str(s).map_err(|_| unreachable!())
}
fn accepts(ty: &Type) -> bool {
<&str as FromSql>::accepts(ty)
}
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
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>
{
let mode_owner_usr = try_get!(p, "owner_user_mode", row)?;
let mode_owner_grp = try_get!(p, "owner_group_mode", row)?;
let mode_everyone = try_get!(p, "everyone_mode", row)?;
let owner_usr = try_get!(p, "owner_user", row)?;
let owner_grp = try_get!(p, "owner_group", row)?;
let path = try_get!(<String> p, "path", row)?;
Ok(Perm { path: Path::parse(path),
owner: (owner_usr, mode_owner_usr),
group: (owner_grp, mode_owner_grp),
everyone: mode_everyone })
}
}
impl Perm {
pub fn actor_can(&self, actor: &Actor, mode: Mode) -> bool {
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 == &self.group.0)
}
}
#[cfg(test)]
mod tests {
use postgres::types::Type;
use super::*;
use crate::model::GroupName;
use crate::postgres::test::Row;
#[test]
fn path_parse() {
assert_eq!(Path::parse("/communities"), Path::Community(Dir::Dir));
assert_eq!(Path::parse("/communities/c1/threads/t1/deleted"),
Path::Community(Dir::Within(CommunityId::from("c1"),
CommunitiesPath::Threads(Dir::Within(ThreadId::from("t1"),
ThreadsPath::Deleted)))));
assert_eq!(Path::parse("/users/1/tag"),
Path::User(Dir::Within(UserId::from("1"), UsersPath::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"), GroupsPath::Members)));
}
#[test]
fn perm_unmarshal() {
let row = Row::<()>::new(vec![("owner_user_mode", Type::TEXT, Box::new("w")),
("owner_group_mode", Type::TEXT, Box::new("w")),
("everyone_mode", Type::TEXT, Box::new("r")),
("owner_user", Type::TEXT, Box::new("u1")),
("owner_group", Type::TEXT, Box::new("g1")),
("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"),
GroupsPath::Members)) };
let admin = Actor { uid: Some(UserId::from("u1")),
groups: vec![GroupId::from("g1")] };
let nonadmin = Actor { uid: Some(UserId::from("u2")),
groups: vec![GroupId::from("g2")] };
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));
}
}

6
src/model/thread.rs Normal file
View File

@ -0,0 +1,6 @@
use crate::newtype;
newtype!(
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct ThreadId(String);
);

458
src/model/user.rs Normal file
View File

@ -0,0 +1,458 @@
use naan::prelude::*;
use postgres::{GenericClient, GenericRow};
use super::{Actor, HashedText, Mode, Path};
use crate::postgres::{try_get, DbError, Postgres, UnmarshalRow};
use crate::repo::{Del, Insert, Page, Patch, ReadMany, ReadOne, Repo};
use crate::{newtype, Email};
newtype!(
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, serde::Serialize, serde::Deserialize)]
pub struct UserId(String);
);
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, serde::Serialize, serde::Deserialize)]
pub struct UserTag {
pub tag: String,
pub discrim: u32,
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, serde::Serialize, serde::Deserialize)]
pub struct User {
pub uid: UserId,
pub tag: UserTag,
pub email: Email,
}
impl UnmarshalRow for User {
fn unmarshal_maybe_prefixed<R, S>(p: Option<S>, row: &R) -> Result<Self, R::Error>
where R: GenericRow,
S: AsRef<str>
{
let p = p.as_ref().map(|p| p.as_ref());
try_get!(<UserId> p, "uid", row).zip(|_: &_| try_get!(<String> p, "tag", row))
.zip(|_: &_| try_get!(<i32> p, "discrim", row))
.zip(|_: &_| try_get!(<String> p, "email", row))
.map(|(((uid, tag), discrim), email)| {
User { uid,
tag: UserTag { tag,
discrim: if discrim < 0 {
0
} else {
discrim as u32
} },
email: Email(email) }
})
}
}
#[derive(Default, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct UserPatch {
pub tag: Option<UserTag>,
pub password: Option<HashedText>,
pub email: Option<Email>,
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct UserInsert {
pub tag: UserTag,
pub password: HashedText,
pub email: Email,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum UserRepoError<E> {
Unauthorized,
PageInvalid,
Other(E),
}
pub trait UserRepo:
Repo<T = User, Id = UserId>
+ Patch<Patch = UserPatch>
+ Insert<Insert = UserInsert>
+ ReadOne
+ ReadMany
+ Del
{
}
pub struct UserRepoImpl<Db: 'static>(pub &'static Db);
impl<Db> Repo for UserRepoImpl<Db> where Db: Postgres + 'static
{
type T = User;
type Error = UserRepoError<DbError<Db>>;
type Id = UserId;
}
impl<Db> ReadOne for UserRepoImpl<Db> where Db: Postgres + 'static
{
fn get(&self, actor: &Actor, id: &Self::Id) -> Result<Option<Self::T>, Self::Error> {
use UserRepoError::*;
static QUERY_LINES: [&'static str; 3] =
["select uid, tag, email",
"from public.usr",
"where uid = human_uuid.huid_of_string($1) and deleted = false"];
let paths = vec![format!("/users/{id}/tag"), format!("/users/{id}/email")].into_iter()
.map(Path::parse)
.collect::<Vec<_>>();
if !self.0
.authorized_all(&paths, Mode::Read, actor)
.map_err(Other)?
{
return Err(Unauthorized);
}
self.0
.with_client(|c| {
c.query_opt(&QUERY_LINES.iter()
.fold(String::new(), |b, a| format!("{b}{a}\n")),
&[&id.as_ref()])
})
.and_then(|opt| {
opt.as_ref()
.map(User::unmarshal)
.sequence::<hkt::ResultOk<_>>()
})
.map_err(Other)
}
}
impl<Db> ReadMany for UserRepoImpl<Db> where Db: Postgres + 'static
{
fn page_valid(&self, page: &Page<Self::Id>) -> bool {
page.limit <= 100
}
fn get_all(&self, actor: &Actor, page: Page<UserId>) -> Result<Vec<Self::T>, Self::Error> {
use UserRepoError::*;
if !self.page_valid(&page) {
return Err(PageInvalid);
}
static QUERY_AFTER_LINES: [&'static str; 6] =
["with after as (select id from public.usr where uid = human_uuid.huid_of_string($1))",
"select human_uuid.huid_to_string(uid), tag, email",
"from public.usr",
"where id > after.id and deleted = false",
"order by id asc",
"limit $2"];
static QUERY_FIRST_LINES: [&'static str; 5] =
["select human_uuid.huid_to_string(uid), tag, email",
"from public.usr",
"where id > after.id and deleted = false",
"order by id asc",
"limit $1"];
let usrs: Vec<User> =
self.0
.with_client(|c| match page.after {
| Some(after) => {
c.query(&QUERY_AFTER_LINES.iter()
.fold(String::new(), |b, a| format!("{b}{a}\n")),
&[&after.as_ref(), &page.limit])
},
| None => c.query(&QUERY_FIRST_LINES.iter()
.fold(String::new(), |b, a| format!("{b}{a}\n")),
&[&page.limit]),
})
.and_then(|vec| vec.iter().map(User::unmarshal).collect())
.map_err(Other)?;
let paths = usrs.iter()
.map(|User { uid, .. }| {
vec![format!("/users/{uid}/tag"), format!("/users/{uid}/email")].into_iter()
.map(Path::parse)
})
.flatten()
.collect::<Vec<_>>();
if !self.0
.authorized_all(&paths, Mode::Read, actor)
.map_err(Other)?
{
Err(Unauthorized)
} else {
Ok(usrs)
}
}
}
impl<Db> Patch for UserRepoImpl<Db> where Db: Postgres + 'static
{
type Patch = UserPatch;
fn patch(&self, actor: &Actor, id: &UserId, patch: UserPatch) -> Result<bool, Self::Error> {
use UserRepoError::*;
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 = human_uuid.huid_of_string($1) and deleted = false"];
let paths = vec!["/users/{id}/tag",
"/users/{id}/email",
"/users/{id}/password"].into_iter()
.map(Path::parse)
.collect::<Vec<_>>();
if !self.0
.authorized_all(&paths, Mode::Write, actor)
.map_err(Other)?
{
return Err(Unauthorized);
}
self.0
.with_client(|c| {
c.execute(&QUERY_LINES.iter()
.fold(String::new(), |b, a| format!("{b}{a}\n")),
&[&id.as_ref(),
&patch.tag.as_ref().map(|t| &t.tag),
&patch.password.as_ref().map(|h| h.as_ref()),
&patch.email.as_ref().map(|e| e.as_ref())])
})
.map(|n| n == 1)
.map_err(Other)
}
}
impl<Db> Insert for UserRepoImpl<Db> where Db: Postgres + 'static
{
type Insert = UserInsert;
fn insert(&self, actor: &Actor, insert: UserInsert) -> Result<UserId, Self::Error> {
use UserRepoError::*;
static QUERY_LINES: [&'static str; 5] = ["insert into public.usr",
" (tag, password, email)",
"values",
" ($2, $3, $4)",
"returning human_uuid.huid_to_string(uid);"];
if !self.0
.authorized(&Path::parse("/users/"), Mode::Write, actor)
.map_err(Other)?
{
return Err(Unauthorized);
}
self.0
.with_client(|c| {
c.query_one(&QUERY_LINES.iter()
.fold(String::new(), |b, a| format!("{b}{a}\n")),
&[&insert.tag.tag,
&insert.password.as_ref(),
&insert.email.as_ref()])
})
.and_then(|r| r.try_get::<_, String>(0))
.map(UserId::from)
.map_err(Other)
}
}
impl<Db> Del for UserRepoImpl<Db> where Db: Postgres + 'static
{
fn del(&self, actor: &Actor, id: &UserId) -> Result<bool, Self::Error> {
use UserRepoError::*;
static QUERY: &'static str = "delete from public.usr where uid = $1 :: uuid;";
if !self.0
.authorized(&Path::parse(format!("/users/{id}/deleted")),
Mode::Write,
actor)
.map_err(Other)?
{
return Err(Unauthorized);
}
self.0
.with_client(|c| c.execute(QUERY, &[&id.as_ref()]))
.map(|n| n == 1)
.map_err(Other)
}
}
impl<Db> UserRepo for UserRepoImpl<Db> where Db: Postgres + 'static {}
#[cfg(test)]
mod tests {
use postgres::types::Type;
use super::{User, UserRepoImpl};
use crate::model::{Actor,
Dir,
GroupId,
HashedText,
Mode,
Path,
Perm,
UserId,
UserInsert,
UserTag};
use crate::postgres::test::{from_sql_owned, Client, Postgres, Row};
use crate::postgres::Postgres as _;
use crate::repo::{Del, Insert, Page, ReadMany, ReadOne, Repo};
use crate::Email;
fn usr_row(usr: User) -> Row<()> {
Row::new(vec![("uid", Type::TEXT, Box::new(usr.uid.to_string())),
("email", Type::TEXT, Box::new(usr.email.to_string())),
("discrim", Type::INT4, Box::new(0)),
("tag", Type::TEXT, Box::new(usr.tag.tag))])
}
#[test]
fn user_repo_get_one() {
let client = || Client::<()> { query_opt: Box::new(|_, q, ps| {
Ok(Some(usr_row(User {uid: UserId::from("1"), tag: UserTag {tag: "foo".into(), discrim: 0}, email: Email::from("foo@bar.baz")})).filter(|_| from_sql_owned::<String>(ps[0]) == String::from("1")))
}),
..Client::default() };
let mut db = Postgres::try_new(|| Ok(client()), 1).unwrap();
db.perms.push(Perm { path: Path::parse("/users/1/tag"),
everyone: Mode::Write,
owner: (UserId::from("root"), Mode::Write),
group: (GroupId::from("admins"), Mode::Write) });
db.perms.push(Perm { path: Path::parse("/users/1/email"),
everyone: Mode::Write,
owner: (UserId::from("root"), Mode::Write),
group: (GroupId::from("admins"), Mode::Write) });
db.perms.push(Perm { path: Path::parse("/users/0/tag"),
everyone: Mode::Write,
owner: (UserId::from("root"), Mode::Write),
group: (GroupId::from("admins"), Mode::Write) });
db.perms.push(Perm { path: Path::parse("/users/0/email"),
everyone: Mode::Write,
owner: (UserId::from("root"), Mode::Write),
group: (GroupId::from("admins"), Mode::Write) });
let repo = UserRepoImpl(unsafe { std::mem::transmute::<_, &'static Postgres<()>>(&db) });
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, _| {
Ok(vec![usr_row(User { uid: UserId::from("1"),
tag: UserTag { tag:
"foo".into(),
discrim: 0 },
email: Email::from("foo@bar.baz") })])
}),
..Client::default() };
let mut db = Postgres::try_new(|| Ok(client()), 1).unwrap();
db.perms.push(Perm { path: Path::parse("/users/"),
everyone: Mode::Write,
owner: (UserId::from("root"), Mode::Write),
group: (GroupId::from("admins"), Mode::Write) });
db.perms.push(Perm { path: Path::parse("/users/1/tag"),
everyone: Mode::Write,
owner: (UserId::from("root"), Mode::Write),
group: (GroupId::from("admins"), Mode::Write) });
db.perms.push(Perm { path: Path::parse("/users/1/email"),
everyone: Mode::Write,
owner: (UserId::from("root"), Mode::Write),
group: (GroupId::from("admins"), Mode::Write) });
let repo = UserRepoImpl(unsafe { std::mem::transmute::<_, &'static Postgres<()>>(&db) });
assert_eq!(repo.get_all(&Actor::default(),
Page { limit: 100,
after: None })
.unwrap()
.len(),
1);
}
#[test]
fn user_repo_del() {
let client =
|| Client::<()> { state: Box::new(false), // already deleted?
execute: Box::new(|c, q, ps| {
if from_sql_owned::<String>(ps[0]) == "1" && !*c.state_mut::<bool>() {
*c.state_mut::<bool>() = true;
Ok(1)
} else {
Ok(0)
}
}),
..Client::default() };
let mut db = Postgres::try_new(|| Ok(client()), 1).unwrap();
db.perms.push(Perm { path: Path::parse("/users/1/deleted"),
everyone: Mode::Write,
owner: (UserId::from("root"), Mode::Write),
group: (GroupId::from("admins"), Mode::Write) });
let repo = UserRepoImpl(unsafe { std::mem::transmute::<_, &'static Postgres<()>>(&db) });
assert_eq!(repo.del(&Actor::default(), &UserId::from("1")).unwrap(),
true);
assert_eq!(repo.del(&Actor::default(), &UserId::from("1")).unwrap(),
false);
assert!(repo.del(&Actor::default(), &UserId::from("2")).is_err());
}
#[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 tag = UserTag { tag: from_sql_owned::<String>(ps[0]),
discrim: 0 };
if tags.contains(&tag) {
Err(())
} else {
tags.push(tag);
Ok(Row::new(vec![("", Type::TEXT, Box::new(tags.len().to_string()))]))
}
}),
..Client::default() };
let mut db = Postgres::try_new(|| Ok(client()), 1).unwrap();
db.perms.push(Perm { path: Path::User(Dir::Dir),
everyone: Mode::Write,
owner: (UserId::from("root"), Mode::Write),
group: (GroupId::from("admins"), Mode::Write) });
let repo = UserRepoImpl(unsafe { std::mem::transmute::<_, &'static Postgres<()>>(&db) });
assert!(repo.insert(&Actor::default(),
UserInsert { tag: UserTag { tag: "foo".into(),
discrim: 0 },
password: HashedText::from("poop"),
email: Email::from("foo@bar.baz") })
.is_ok());
assert!(repo.insert(&Actor::default(),
UserInsert { tag: UserTag { tag: "foo".into(),
discrim: 0 },
password: HashedText::from("poop"),
email: Email::from("foo@bar.baz") })
.is_err());
assert!(repo.insert(&Actor::default(),
UserInsert { tag: UserTag { tag: "bar".into(),
discrim: 0 },
password: HashedText::from("poop"),
email: Email::from("bar@bar.baz") })
.is_ok());
}
}

122
src/model/user_session.rs Normal file
View File

@ -0,0 +1,122 @@
use std::net::{IpAddr, SocketAddr};
use naan::prelude::*;
use postgres::{GenericClient, GenericRow};
use super::{Actor, GroupRepo, User};
use crate::newtype;
use crate::postgres::{DbError, Error, Postgres, UnmarshalRow};
use crate::repo::Ext;
newtype!(
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct UserSession(String);
);
#[derive(Default, Debug, Clone, PartialEq, Eq)]
pub struct LoginOpts {
pub remember: bool,
pub location: Option<String>,
pub device: Option<String>,
pub ip: Option<IpAddr>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LoginError<E> {
NoSuchUser,
Other(E),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ValidateError<E> {
Expired,
Invalid,
Other(E),
}
pub trait UserSessionExt: Ext {
fn touch(&self, session: UserSession) -> Result<Actor, ValidateError<Self::Error>>;
fn login<TE, P>(&self,
tag_or_email: TE,
password: P,
opts: LoginOpts)
-> Result<UserSession, LoginError<Self::Error>>
where TE: AsRef<str>,
P: AsRef<str>;
}
pub struct UserSessionExtImpl<Db: 'static, Group: 'static>(pub &'static Db, pub &'static Group);
impl<Db, G> Ext for UserSessionExtImpl<Db, G>
where Db: Postgres,
G: GroupRepo
{
type Error = DbError<Db>;
}
impl<Db, G> UserSessionExt for UserSessionExtImpl<Db, G>
where Db: Postgres,
G: GroupRepo<Error = Self::Error>
{
fn touch(&self, session: UserSession) -> Result<Actor, ValidateError<Self::Error>> {
let query = "select public.usr_session_touch(public.usr_session_key_of_string($1))";
self.0
.with_client(|c| {
c.query_one(query, &[&session.as_ref()])
.and_then(|r| User::unmarshal(&r))
})
.map_err(|e| {
if e.message() == Some("usr_session_expired".to_string()) {
ValidateError::Expired
} else if e.message() == Some("usr_session_invalid".to_string()) {
ValidateError::Invalid
} else {
ValidateError::Other(e)
}
})
.zip(|u: &User| self.1.groups(&u.uid).map_err(ValidateError::Other))
.map(|(u, gs)| Actor { uid: Some(u.uid),
groups: gs })
}
fn login<TE, P>(&self,
tag_or_email: TE,
password: P,
opts: LoginOpts)
-> Result<UserSession, LoginError<DbError<Db>>>
where TE: AsRef<str>,
P: AsRef<str>
{
const QUERY_LINES: [&'static str; 9] =
["select public.usr_session_key_to_string(s)",
" from public.usr_session_login",
" ( tag_or_email => public.usr_tag_or_email_of_string($1)",
" , password => public.hashed_text_of_string($2)",
" , remember => $3",
" , location => $4",
" , device => $5",
" , ip => $6",
" ) s"];
self.0
.with_client(|c| {
c.query_one(&QUERY_LINES.iter()
.fold(String::new(), |b, a| format!("{b}{a}\n")),
&[&tag_or_email.as_ref(),
&password.as_ref(),
&opts.remember,
&opts.location,
&opts.device,
&opts.ip])
.and_then(|r| r.try_get::<_, UserSession>(0))
})
.map_err(|e: Db::Error| {
if e.message() == Some("incorrect_password".to_string()) {
LoginError::NoSuchUser
} else {
LoginError::Other(e)
}
})
}
}

View File

@ -1,263 +0,0 @@
use std::str::FromStr;
use naan::prelude::*;
use crate::group::{Group, GroupId};
use crate::postgres::{try_get, 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());
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),
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(|_: &_| 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),
group: (GroupId::from(group), group_mode),
everyone: everyone_mode,
})
}
}
impl Perm {
pub fn actor_can(&self, actor: &Actor, mode: Mode) -> bool {
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() {
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)));
}
#[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

@ -3,18 +3,35 @@ use std::pin::Pin;
use std::sync::{Mutex, MutexGuard};
use postgres::types::FromSql;
use postgres::GenericRow;
use postgres::{GenericClient, GenericRow};
use rand::Rng;
use crate::perm::Actor;
use crate::model::{Actor, Mode, Path, Perm};
pub type DbError<Pg> = <<Pg as Postgres>::Client as postgres::GenericClient>::Error;
pub trait Error {
fn message(&self) -> Option<String>;
}
impl Error for postgres::Error {
fn message(&self) -> Option<String> {
self.as_db_error().map(|db| db.message().to_string())
}
}
impl Error for () {
fn message(&self) -> Option<String> {
None
}
}
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());
let col = $p.as_ref()
.filter(|s| !str::is_empty(s.as_ref()))
.map(|s| format!("{}.{}", AsRef::<str>::as_ref(s), $col))
.unwrap_or($col.to_string());
$row.try_get::<_, $t>(col.as_str())
}};
($p:expr, $col:expr, $row:expr) => {{
@ -53,7 +70,8 @@ pub struct ConnectionParams {
pub trait Postgres
where Self: Send + Sync + Sized + 'static
{
type Client: postgres::GenericClient;
type Client: postgres::GenericClient<Error = Self::Error>;
type Error: Error + core::fmt::Debug;
fn try_new<F>(connect: F, pool_size: usize) -> Result<Self, DbError<Self>>
where F: Fn() -> Result<Self::Client, DbError<Self>>;
@ -61,7 +79,43 @@ 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>>;
fn authorized(&self, path: &Path, wants_to: Mode, actor: &Actor) -> Result<bool, DbError<Self>> {
const Q: &'static str = "select * from public.perm where path = $1";
let got_perm = self.with_client(|c| c.query_opt(Q, &[&path.to_string()]))?;
Ok(match got_perm {
| None => false,
| Some(perm) => Perm::unmarshal(&perm)?.actor_can(actor, wants_to),
})
}
fn authorized_all(&self,
paths: &[Path],
wants_to: Mode,
actor: &Actor)
-> Result<bool, DbError<Self>> {
if paths.iter().any(|p| matches!(p, Path::Unknown(_))) {
return Ok(false);
}
let set = paths.iter()
.map(Path::to_string)
.fold(String::new(), |b, a| {
if b.is_empty() {
a.to_string()
} else {
format!("{b}, {a}")
}
});
let q = format!("select * from public.perm where path in {set}");
let pass = self.with_client(|c| c.query(&q, &[]))?
.iter()
.map(Perm::unmarshal)
.collect::<Result<Vec<Perm>, _>>()?
.iter()
.all(|p| p.actor_can(actor, wants_to));
Ok(pass)
}
}
pub struct PostgresImpl<C> {
@ -82,9 +136,11 @@ impl<C> PostgresImpl<C> {
impl<C> Postgres for PostgresImpl<C>
where Self: Send + Sync,
C: 'static + postgres::GenericClient
C: 'static + postgres::GenericClient,
C::Error: Error
{
type Client = C;
type Error = C::Error;
fn try_new<F>(connect: F, pool_size: usize) -> Result<Self, C::Error>
where F: Fn() -> Result<C, C::Error>
@ -114,22 +170,19 @@ 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)]
pub mod test {
use std::any::Any;
use std::marker::PhantomData;
use std::ops::DerefMut;
use std::sync::Mutex;
pub fn from_sql_owned<T>(to: &dyn ToSql) -> T
where T: 'static + for<'a> FromSql<'a> + postgres::types::Typed
{
let mut buf = BytesMut::with_capacity(128);
let mut buf = BytesMut::with_capacity(256);
to.to_sql(&T::TYPE, &mut buf).unwrap();
T::from_sql(&T::TYPE, &buf).unwrap()
}
@ -202,7 +255,8 @@ pub mod test {
use postgres::types::{FromSql, ToSql, Type};
use postgres::{Column, GenericClient, GenericRow};
use super::{Postgres, PostgresImpl};
use super::{Postgres as _, PostgresImpl};
use crate::model::{Actor, Mode, Path, Perm};
pub struct Row<E> {
pub columns: Vec<Column>,
@ -217,7 +271,13 @@ pub mod test {
.collect(),
values: cols.into_iter()
.map(|(_, ty, val)| {
let mut bs = BytesMut::with_capacity(128);
let mut bs = if ty == Type::INT2 {
BytesMut::with_capacity(2)
} else if ty == Type::INT4 {
BytesMut::with_capacity(4)
} else {
BytesMut::with_capacity(128)
};
val.to_sql(&ty, &mut bs).unwrap();
bs
})
@ -257,6 +317,54 @@ pub mod test {
}
}
pub struct Postgres<E> {
client: Mutex<Client<E>>,
pub perms: Vec<Perm>,
}
unsafe impl<E> Send for Postgres<E> {}
unsafe impl<E> Sync for Postgres<E> {}
impl<E> super::Postgres for Postgres<E> where E: core::fmt::Debug + 'static + super::Error
{
type Client = Client<E>;
type Error = E;
fn try_new<F>(connect: F, _: usize) -> Result<Self, super::DbError<Self>>
where F: Fn() -> Result<Self::Client, super::DbError<Self>>
{
Ok(Self { client: Mutex::new(connect()?),
perms: vec![] })
}
fn with_client<F, R>(&self, f: F) -> Result<R, super::DbError<Self>>
where F: FnOnce(&mut Self::Client) -> Result<R, super::DbError<Self>>
{
f(self.client.lock().unwrap().deref_mut())
}
fn authorized(&self,
path: &Path,
wants_to: Mode,
actor: &Actor)
-> Result<bool, super::DbError<Self>> {
Ok(self.perms
.iter()
.find(|p| &p.path == path)
.map(|p| p.actor_can(actor, wants_to))
.unwrap_or(false))
}
fn authorized_all(&self,
paths: &[Path],
wants_to: Mode,
actor: &Actor)
-> Result<bool, super::DbError<Self>> {
Ok(paths.iter()
.all(|p| self.authorized(p, wants_to, actor).unwrap()))
}
}
#[non_exhaustive]
pub struct Client<E> {
pub __phantom: PhantomData<E>,

8
src/rep_payload.rs Normal file
View File

@ -0,0 +1,8 @@
use std::collections::HashMap;
#[derive(serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
pub struct RepPayload<T> {
#[serde(flatten)]
pub t: T,
pub links: HashMap<String, String>,
}

View File

@ -1,19 +1,44 @@
use crate::perm::Actor;
use crate::model::Actor;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Page<Id> {
pub limit: u32,
pub after: Option<Id>,
}
pub trait Repo: Send + Sync {
type T;
type Patch;
type Insert;
type Error: core::fmt::Debug;
type Id: AsRef<str>;
}
pub trait ReadOne: Send + Sync + Repo {
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>;
}
pub trait ReadMany: Send + Sync + Repo {
fn page_valid(&self, page: &Page<Self::Id>) -> bool;
fn get_all(&self, actor: &Actor, page: Page<Self::Id>) -> Result<Vec<Self::T>, Self::Error>;
}
pub trait Patch: Send + Sync + Repo {
type Patch;
fn patch(&self, actor: &Actor, id: &Self::Id, state: Self::Patch) -> Result<bool, Self::Error>;
}
pub trait Insert: Send + Sync + Repo {
type Insert;
fn insert(&self, actor: &Actor, state: Self::Insert) -> Result<Self::Id, Self::Error>;
}
pub trait Del: Send + Sync + Repo {
fn del(&self, actor: &Actor, id: &Self::Id) -> Result<bool, Self::Error>;
}
pub trait Dump: Send + Sync + Repo {
fn dump(&self, actor: &Actor) -> Result<Vec<Self::T>, Self::Error>;
}
/// An entity that has some operations which rely on
/// an external service
pub trait Ext: Send + Sync {

View File

@ -6,7 +6,7 @@ use toad_msg::{ContentFormat, MessageOptions};
pub struct ReqPayload<T> {
#[serde(flatten)]
pub t: T,
pub session: String,
pub session: Option<String>,
}
impl<T> ReqPayload<T> where T: DeserializeOwned

View File

@ -1,267 +0,0 @@
use naan::prelude::*;
use postgres::{GenericClient, GenericRow};
use crate::hashed_text::HashedText;
use crate::perm::Actor;
use crate::postgres::{try_get, DbError, Postgres, UnmarshalRow};
use crate::repo::Repo;
use crate::{newtype, Email};
newtype!(
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct UserId(String);
);
newtype!(
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct UserTag(String);
);
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct User {
uid: UserId,
tag: UserTag,
password: HashedText,
email: Email,
}
impl UnmarshalRow for User {
fn unmarshal_maybe_prefixed<R, S>(p: Option<S>, row: &R) -> Result<Self, R::Error>
where R: GenericRow,
S: AsRef<str>
{
let p = p.as_ref().map(|p| p.as_ref());
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) }
})
}
}
#[derive(Default, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct UserPatch {
tag: Option<UserTag>,
password: Option<HashedText>,
email: Option<Email>,
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct UserInsert {
tag: UserTag,
password: HashedText,
email: Email,
}
pub trait UserRepo: Repo<T = User, Patch = UserPatch, Id = UserId> {}
pub struct UserRepoImpl<Db: 'static>(pub &'static Db);
impl<Db> Repo for UserRepoImpl<Db> where Db: Postgres + 'static
{
type T = User;
type Patch = UserPatch;
type Insert = UserInsert;
type Error = DbError<Db>;
type Id = UserId;
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 and deleted = false";
self.0
.with_client(|c| c.query_opt(QUERY, &[&id.as_ref()]))
.and_then(|opt| {
opt.as_ref()
.map(User::unmarshal)
.sequence::<hkt::ResultOk<_>>()
})
}
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, 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 and deleted = false"];
self.0
.with_client(|c| {
c.execute(&QUERY_LINES.iter()
.fold(String::new(), |b, a| format!("{b}{a}\n")),
&[&id.as_ref(),
&patch.tag.as_ref().map(|t| t.as_ref()),
&patch.password.as_ref().map(|h| h.as_ref()),
&patch.email.as_ref().map(|e| e.as_ref())])
})
.map(|n| n == 1)
}
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",
" ($2, $3, $4)",
"returning uid;"];
self.0
.with_client(|c| {
c.query_one(&QUERY_LINES.iter()
.fold(String::new(), |b, a| format!("{b}{a}\n")),
&[&insert.tag.as_ref(),
&insert.password.as_ref(),
&insert.email.as_ref()])
})
.and_then(|r| r.try_get::<_, String>(0))
.map(UserId::from)
}
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
.with_client(|c| c.execute(QUERY, &[&id.as_ref()]))
.map(|n| n == 1)
}
}
impl<Db> UserRepo for UserRepoImpl<Db> where Db: Postgres + 'static {}
#[cfg(test)]
mod tests {
use postgres::types::Type;
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, UserInsert, UserTag};
use crate::Email;
fn usr_row(usr: User) -> Row<()> {
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]
fn user_repo_get_one() {
let client = || Client::<()> { query_opt: Box::new(|_, q, ps| {
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() };
let db = PostgresImpl::try_new(|| Ok(client()), 1).unwrap();
let repo =
UserRepoImpl(unsafe { std::mem::transmute::<_, &'static PostgresImpl<Client<()>>>(&db) });
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, _| {
Ok(vec![usr_row(User { uid: UserId::from("1"),
tag: UserTag::from("foo"),
email: Email::from("foo@bar.baz"),
password: HashedText::from("XXX") })])
}),
..Client::default() };
let db = PostgresImpl::try_new(|| Ok(client()), 1).unwrap();
let repo =
UserRepoImpl(unsafe { std::mem::transmute::<_, &'static PostgresImpl<Client<()>>>(&db) });
assert_eq!(repo.get_all(&Actor::default()).unwrap().len(), 1);
}
#[test]
fn user_repo_del() {
let client =
|| Client::<()> { state: Box::new(false), // already deleted?
execute: Box::new(|c, q, ps| {
if from_sql_owned::<String>(ps[0]) == "1" && !*c.state_mut::<bool>() {
*c.state_mut::<bool>() = true;
Ok(1)
} else {
Ok(0)
}
}),
..Client::default() };
let db = PostgresImpl::try_new(|| Ok(client()), 1).unwrap();
let repo =
UserRepoImpl(unsafe { std::mem::transmute::<_, &'static PostgresImpl<Client<()>>>(&db) });
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>::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]));
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();
let repo =
UserRepoImpl(unsafe { std::mem::transmute::<_, &'static PostgresImpl<Client<()>>>(&db) });
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());
}
}

View File

@ -1,66 +0,0 @@
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!()
}
}

View File

@ -1 +0,0 @@