From eaeb2939154fb47fb3fac32e30f79d41c2f0da4b Mon Sep 17 00:00:00 2001 From: Waylon Cude Date: Sun, 24 May 2026 22:53:52 -0700 Subject: [PATCH] State implementation and initial db work State system is fully implemented and passing tests. I added in a handful of unit tests to test most of the available operations. Other work has involved getting the interface between client and server right and starting on the actual message handler. This involves starting work on the db as well --- Cargo.lock | 47 +- Cargo.toml | 2 + src/client.rs | 447 ++++++++++------- src/config.rs | 3 + src/db/mod.rs | 66 +++ src/db/models.rs | 7 + src/main.rs | 56 ++- src/state.rs | 1253 ++++++++++++++++++++++++++++++++++++++++++++-- 8 files changed, 1639 insertions(+), 242 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 30c3ea3..2a4cc04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -81,6 +81,19 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bcrypt" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ae5479c93d3720e4c1dbd6b945b97457c50cb672781104768190371df1a905" +dependencies = [ + "base64", + "blowfish", + "getrandom 0.4.2", + "subtle", + "zeroize", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -102,6 +115,16 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "blowfish" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ce3946557b35e71d1bbe07ec385073ce9eda05043f95de134eb578fcf1a298" +dependencies = [ + "byteorder", + "cipher", +] + [[package]] name = "bumpalo" version = "3.20.2" @@ -159,6 +182,16 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "cipher" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clap" version = "4.6.1" @@ -225,6 +258,7 @@ dependencies = [ name = "confetti" version = "0.1.0" dependencies = [ + "bcrypt", "clap", "ctrlc-async", "diesel", @@ -283,9 +317,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ "hybrid-array", ] @@ -686,6 +720,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "hybrid-array", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" diff --git a/Cargo.toml b/Cargo.toml index 8efe6b9..a3a6273 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,3 +19,5 @@ diesel-async = { version = "0.9.0", features = ["postgres","deadpool"] } ctrlc-async = { version = "3.2.2", features = ["termination"] } rmp-serde = "1.3.1" diesel-derive-composite = "0.1.0" +bcrypt = "0.19.1" +#postcard = {version = "1.1.3", features = ["use-std"]} diff --git a/src/client.rs b/src/client.rs index e2fc3f1..25bab64 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,16 +1,16 @@ -use fedichat::RoomId; use fedichat::message::{Relevance,TaggedMessage}; use fedichat::client::{ClientMessage,SignedClientMessage,ServerMessage,ServerError}; -use fedichat::state::StatePath; use diesel_async::AsyncPgConnection; -use diesel_async::pooled_connection::deadpool::Pool; +use diesel_async::pooled_connection::deadpool::{Pool,Object}; use rmp_serde; use std::collections::HashSet; use std::sync::{Arc}; use thiserror::Error; use tokio::sync::{broadcast,mpsc,RwLock}; use tokio::select; +use crate::Coordinate; +use crate::db; use crate::state::State; use tracing::{warn,error,debug}; @@ -73,6 +73,7 @@ impl Client { } pub async fn run(mut self,config: crate::config::Config) { + // This can probably use a refactoring at some point, this function is huge loop { // Wait for either a client to send a message, a message that we might need to @@ -104,13 +105,14 @@ impl Client { } } }; - let result = match self.handle_message(message).await { + let result = match self.handle_message(&config,message).await { Ok(res) => res, Err(e) => { - warn!("Failed to handle client message with error {}",e); - continue; + warn!("Failed to handle client message with error {:?}",e); + ServerMessage::Error(e) } }; + // TODO: This needs to override the default options to use a map let buf = match rmp_serde::to_vec(&result) { Ok(b) => b, Err(e) => { @@ -137,6 +139,8 @@ impl Client { }; // Check if the message is in our subscriptions // If it is then send it down to the user + // Probably should check that we're still in a room maybe? Not sure how + // kicks are going to work. if let Some(relevance) = result.get_relevance() { if self.subscriptions.contains(&relevance) { match self.message_ack.send(relevance).await { @@ -148,6 +152,7 @@ impl Client { }; let send = self.quic_connection.open_uni(); // This is duplicated code, maybe could break into a function + // TODO: This needs to override the default options to use a map let buf = match rmp_serde::to_vec(&result) { Ok(b) => b, Err(e) => { @@ -165,7 +170,6 @@ impl Client { } } } - unimplemented!() } _result = self.close_handle.recv() => { // Maybe TODO do I need to check the result? @@ -178,8 +182,15 @@ impl Client { } + // This opens up a quic connection to the remote server, serializes our message, and + // processes the response. Should use a long-lived connection but for the prototype its + // going to open a new one each time. + async fn forward(&self, server: &fedichat::ServerAddr, message: TaggedMessage) -> Result{ + unimplemented!() + } + // Handles message and send back the right response. - async fn handle_message(&mut self, message: SignedClientMessage) -> Result { + async fn handle_message(&mut self, config: &crate::config::Config, message: SignedClientMessage) -> Result { // 3 states // waiting on challenge // waiting on auth @@ -197,184 +208,276 @@ impl Client { } // hmm users should probably be able to update state and do everything else still - // right?? Is forcing the to immediately complete the challenge too much? + // right?? Is forcing them to immediately complete the challenge too much? } else { unimplemented!() - } } else if let Some(ref username) = self.username { + let user = fedichat::User { + name: username.clone(), + server: config.hostname.clone() + + }; use fedichat::client::ClientMessage::*; - match message.message { + // Forward the message if it is addressed to a remote server + if let Some(servername) = message.target.clone() + && servername != fedichat::ServerAddr(config.hostname.clone()) { - Auth{ - username: _username, - password: _password - } => { - return Ok(ServerMessage::Error(ServerError::AlreadyAuthenticated)) - }, - // Maybe ask for email too? Or a potential invite code - UserCreate { - username: _username, - password: _password, - } => { - return Ok(ServerMessage::Error(ServerError::AlreadyAuthenticated)) - }, - // Used to require accounts to complete some kind of challenge. Simplest - // is giving a password/invite code to create an account or join a room - ChallengeAnswer { - response: _, - } => { - return Ok(ServerMessage::Error(ServerError::NotInChallenge)) - }, + if message.message.is_forwardable() { + let user = self.get_user(config)?; + self.forward(&servername,message.tag(user,fedichat::ServerAddr(config.hostname.clone()))).await + } else { + Err(ServerError::MessageNotForwardable) + } + } else { + match message.message { - // Should it be one message type or multiple? How does end-to-end - // encryption work here? It could be done in a hacky way with extra tags - Message { - body, - room_id, - } => { - unimplemented!() - }, - // Private message/invite mechanism - MessagePost { - body, - user, - } => {unimplemented!()}, - // Replace the body of the message with a new one - MessageEdit { - body, - id, - room_id, - } => {unimplemented!()}, - MessageDelete { - id, - room_id, - } => {unimplemented!()}, - // State Actions - StateCreate { - room_id, - path, - ty, - permissions, - } => {unimplemented!()}, - StateWrite { - room_id, - path, - content, - } => {unimplemented!()}, - StateDelete { - room_id, - path, - } => {unimplemented!()}, - StateAppend { - room_id, - path, - content, - } => {unimplemented!()}, - StateMove { - room_id, - path, - target, - } => {unimplemented!()}, - StateRead { - room_id, - path, - } => {unimplemented!()}, - PermissionAdd { - permission, - } => {unimplemented!()}, - PermissionDelete { - permission, - } => {unimplemented!()}, - // Groups really should have a way to add permissions by user - // specifically for who can join or invite others - // - // Maybe make a group -> role -> member hierarchy? - // - // - // Could always do this through a bot that owns a group?? - GroupCreate { - group, - users, - } => {unimplemented!()}, - // Only the creator of a group or a server admin can delete groups - GroupDelete { - group, - } => {unimplemented!()}, - // Only the creator of a group or a server admin can delete groups - // same with adding, though there should be a way to add group officers - // at some point - GroupUserAdd { - group, - users, - } => {unimplemented!()}, - GroupUserRemove { - group, - users, - } => {unimplemented!()}, - GroupRoleCreate { - group, - role, - } => {unimplemented!()}, - GroupRoleDelete { - group, - role, - } => {unimplemented!()}, - GroupRoleUserAdd { - group, - role, - users, - } => {unimplemented!()}, - GroupRoleUserRemove { - group, - role, - users, - } => {unimplemented!()}, - // Should work like discord roles - // Can control who can invite to the group - // Can be used with permissions to make rooms that are private for individual roles - GroupRolePowerAdd { - group, - role, - power, - } => {unimplemented!()}, - GroupRolePowerRemove { - group, - role, - power, - } => {unimplemented!()}, - // Returns an ID to use for message sending - // The server can potentially use the current username to associate media uploads - // with users - MediaUpload { - bytes, - } => {unimplemented!()}, - // Join and subscribe - Join { - room_id, - } => {unimplemented!()}, - SubscribeMessages { - room_id, - } => {unimplemented!()}, - SubscribeState { - room_id, - state, - } => {unimplemented!()}, - FetchMessages { - count, - end, - } => {unimplemented!()} + Auth{ + username: _username, + password: _password + } => { + return Ok(ServerMessage::Error(ServerError::AlreadyAuthenticated)) + }, + // Maybe ask for email too? Or a potential invite code + UserCreate { + username: _username, + password: _password, + } => { + return Ok(ServerMessage::Error(ServerError::AlreadyAuthenticated)) + }, + // Used to require accounts to complete some kind of challenge. Simplest + // is giving a password/invite code to create an account or join a room + ChallengeAnswer { + response: _, + } => { + return Ok(ServerMessage::Error(ServerError::NotInChallenge)) + }, + + // Should it be one message type or multiple? How does end-to-end + // encryption work here? It could be done in a hacky way with extra html tags + Message { + body, + room_id, + id, + } => { + unimplemented!() + }, + // Private message/invite mechanism + MessagePost { + body, + user, + } => {unimplemented!()}, + // Replace the body of the message with a new one + MessageEdit { + body, + id, + room_id, + } => {unimplemented!()}, + MessageDelete { + id, + room_id, + } => {unimplemented!()}, + // State Actions + StateCreate { + room_id, + path, + content, + permissions, + } => {unimplemented!()}, + StateWrite { + room_id, + path, + content, + } => {unimplemented!()}, + StateDelete { + room_id, + path, + } => {unimplemented!()}, + StateAppend { + room_id, + path, + content, + } => {unimplemented!()}, + StateMove { + room_id, + path, + target, + } => {unimplemented!()}, + StateRead { + room_id, + path, + } => {unimplemented!()}, + PermissionAdd { + permission, + path, + room_id + } => {unimplemented!()}, + PermissionRead { + path, + room_id + } => {unimplemented!()}, + PermissionDelete { + permission, + path, + room_id + } => {unimplemented!()}, + // Groups really should have a way to add permissions by user + // specifically for who can join or invite others + // + // Maybe make a group -> role -> member hierarchy? + // + // + // Could always do this through a bot that owns a group?? + GroupCreate { + group, + users, + } => {unimplemented!()}, + // Only the creator of a group or a server admin can delete groups + GroupDelete { + group, + } => {unimplemented!()}, + // Only the creator of a group or a server admin can delete groups + // same with adding, though there should be a way to add group officers + // at some point + GroupUserAdd { + group, + users, + } => {unimplemented!()}, + GroupUserRemove { + group, + users, + } => {unimplemented!()}, + GroupRoleCreate { + group, + role, + } => {unimplemented!()}, + GroupRoleDelete { + group, + role, + } => {unimplemented!()}, + GroupRoleUserAdd { + group, + role, + users, + } => {unimplemented!()}, + GroupRoleUserRemove { + group, + role, + users, + } => {unimplemented!()}, + // Should work like discord roles + // Can control who can invite to the group + // Can be used with permissions to make rooms that are private for individual roles + GroupRolePowerAdd { + group, + role, + power, + } => {unimplemented!()}, + GroupRolePowerRemove { + group, + role, + power, + } => {unimplemented!()}, + // Returns an ID to use for message sending + // The server can potentially use the current username to associate media uploads + // with users + MediaUpload { + bytes, + } => {unimplemented!()}, + // Join and subscribe + RoomJoin { + room_id, + } => { + let coords: Coordinate = room_id.clone().into(); + + if !self.statehandle.read().await.room_exists(coords.clone()) { + Ok(ServerMessage::Error(ServerError::RoomNotFound(room_id.clone()))) + } else { + self.statehandle.write().await.join(coords,user).await?; + Ok(ServerMessage::Ok) + } + }, + RoomCreate { + room_id, + } => { + let coords: Coordinate = room_id.clone().into(); + + if self.statehandle.read().await.room_exists(coords.clone()) { + Ok(ServerMessage::Error(ServerError::RoomExists(room_id.clone()))) + } else { + // This surely does not deadlock + self.statehandle.write().await.create_room(coords,user)?; + Ok(ServerMessage::Ok) + } + }, + SubscribeMessages { + room_id, + } => { + // TODO: Has to check if user is joined to room first + self.subscriptions.insert(Relevance::Message(room_id)); + Ok(ServerMessage::Ok) + }, + SubscribeState { + room_id, + state, + } => { + // TODO: Has to check if user is joined to room first + if self.statehandle.read().await.is_joined(room_id.clone().into(),user).await { + self.subscriptions.insert(Relevance::State(room_id,state)); + Ok(ServerMessage::Ok) + } else { + Err(ServerError::NotJoined(room_id)) + } + }, + FetchMessages { + count, + end, + } => {unimplemented!()} + + + } } - } else if let ClientMessage::Auth {username,password} = message.message { - unimplemented!() - } else if let ClientMessage::UserCreate {username,password} = message.message { - unimplemented!() + } else if let ClientMessage::Auth {ref username,ref password} = message.message { + match db::verify_user(self.get_db().await?,username,password).await? { + true => { + self.username = Some(username.clone()); + Ok(ServerMessage::Ok) + }, + false => { + Err(ServerError::AuthenticationFailed) + } + } + } else if let ClientMessage::UserCreate {username,password} = &message.message { + if let Some(ref code) = config.account_creation_code { + self.in_challenge = Some((code.to_string(),message)); + + Err(ServerError::ChallengeInviteCode) + } else { + db::maybe_create_user(self.get_db().await ?,username,password).await + } } else { return Ok(ServerMessage::Error(ServerError::NotAuthenticated)); } } + fn get_user(&self,config: &crate::config::Config) -> Result { + Ok(fedichat::User { + name: self.username.clone().ok_or(ServerError::NotAuthenticated)?, + server: config.hostname.clone() + }) + } + async fn get_db(&self) -> Result,ServerError> { + match self.db_handle.get().await { + Ok(conn) => Ok(conn), + Err(e) => { + error!("Could not connect to database server: {e}"); + // This error is not relevant to the client in any way + Err(ServerError::Generic) + } + } + + } } diff --git a/src/config.rs b/src/config.rs index 44dbd8a..f7f9460 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,6 +7,9 @@ use thiserror::Error; #[derive(Clone,Serialize,Deserialize)] pub struct Config { pub hostname: String, + //NOTE: Changing the federation port breaks federation + // Changing the client port is also probably a bad idea. There might + // need to be .wellknown support at some point pub port: u16, pub federation_port: u16, pub listen_address: String, diff --git a/src/db/mod.rs b/src/db/mod.rs index d5cbad7..2aa57d9 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,2 +1,68 @@ +use diesel::prelude::*; pub mod models; pub mod schema; + + +use bcrypt::{DEFAULT_COST, hash, verify}; +use diesel_async::AsyncPgConnection; +use diesel_async::RunQueryDsl;//, AsyncConnection}; +use diesel_async::pooled_connection::deadpool::Object; +use fedichat::client::{ServerMessage,ServerError}; +use tracing::{instrument,warn}; + +#[instrument(skip_all)] +pub async fn maybe_create_user(mut connection: Object, username: &str, password: &str) -> Result { + let password = match hash(password,DEFAULT_COST) { + Ok(val) => val, + Err(e) => { + warn!("Error encountered while generating password hash"); + warn!("{e}"); + return Err(ServerError::Generic) + } + }; + let username = username.to_string(); + let user = models::NewUser{ username, password}; + let result = diesel::insert_into(schema::users::table) + .values(&user) + .execute(&mut *connection) + .await; + + match result { + // TODO: might need to check this? I don't know what it means + Ok(_) => Ok(ServerMessage::Ok), + // TODO: Probably actually check the error + Err(_e) => Err(ServerError::UserAlreadyExists) + } +} + +/// Try to authenticate a user against information in the database +#[instrument(skip_all)] +pub async fn verify_user(mut connection: Object, user: &str, pass: &str) -> Result { + use schema::users::dsl::*; + + let result = users + .filter(username.eq(user)) + .select(models::User::as_select()) + .first(&mut *connection) + .await; + + + + match result { + // If we have more than 0 rows then authentication is successful + Ok(user) => match verify(pass,&user.password) { + Ok(val) => Ok(val), + Err(e) => { + warn!("Error encountered while generating password hash"); + warn!("{e}"); + return Err(ServerError::Generic) + } + + }, + // TODO: Probably actually check the error + _ => Err(ServerError::AuthenticationFailed) + } + + + +} diff --git a/src/db/models.rs b/src/db/models.rs index 7635197..7187e9e 100644 --- a/src/db/models.rs +++ b/src/db/models.rs @@ -55,6 +55,13 @@ pub struct User { pub password: String, } +#[derive(Insertable)] +#[diesel(table_name = crate::db::schema::users)] +pub struct NewUser { + pub username: String, + pub password: String, +} + #[derive(Queryable, Selectable)] #[diesel(table_name = crate::db::schema::messages)] pub struct Messages { diff --git a/src/main.rs b/src/main.rs index 9727ec7..a287ef0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,6 @@ mod state; use diesel_async::pooled_connection::AsyncDieselConnectionManager; use diesel_async::pooled_connection::deadpool::Pool; -use diesel_async::{AsyncConnection,AsyncPgConnection}; use quinn::rustls::pki_types::{PrivateKeyDer,CertificateDer,pem::PemObject}; use quinn::Endpoint; use std::io; @@ -21,25 +20,32 @@ use tokio::sync::{RwLock,broadcast,mpsc}; use crate::config::Config; -use crate::state::State; +use crate::state::{State,StateError}; -#[derive(Hash,Eq,PartialEq,Clone,Serialize,Deserialize)] +#[derive(Hash,Eq,PartialEq,Clone,Serialize,Deserialize,Debug)] pub struct Coordinate(Vec); +impl From for Coordinate { + fn from(other: fedichat::RoomId) -> Coordinate { + Coordinate(other.coordinates) + } +} + #[tokio::main] #[instrument] async fn main() -> ExitCode { + // NOTE: This doesn't work as you can only initialize the global logger once // Initial logger so we have something during config - tracing::subscriber::set_global_default( - tracing_subscriber::fmt().with_max_level(Level::WARN).finish() - ).expect("Failed to setup logger"); + //tracing::subscriber::set_global_default( + // tracing_subscriber::fmt().with_max_level(Level::WARN).finish() + //).expect("Failed to setup logger"); // Read in config let config = match Config::load() { Ok(c) => c, Err(e) => { - error!("Problem while reading config file"); - error!("{:?}",e); + eprintln!("Problem while reading config file"); + eprintln!("{:?}",e); return ExitCode::FAILURE; } }; @@ -54,7 +60,7 @@ async fn main() -> ExitCode { "warn" => Level::WARN, "error" => Level::ERROR, _ => { - warn!("Invalid loglevel in config: {}",&loglevel); + eprintln!("Invalid loglevel in config: {}",&loglevel); Level::INFO }, } @@ -120,29 +126,25 @@ async fn main() -> ExitCode { let state = match State::load_from_file(&config.statefile) { Ok(state) => state, // Create file if it does not exist - Err(e) => { - match e.kind() { - io::ErrorKind::NotFound => { - match fs::File::create(&config.statefile) { - // If the statefile is writable then create an empty state - // and use that - Ok(_) => State::new(), - Err(e) => { - error!("Could not open or create statefile. Check your config."); - error!("{:?}",e); - return ExitCode::FAILURE; - - } - } - - }, - _ => { + Err(StateError::IOError(e)) if e.kind() == io::ErrorKind::NotFound => { + match fs::File::create(&config.statefile) { + // If the statefile is writable then create an empty state + // and use that + Ok(_) => State::new(), + Err(e) => { error!("Could not open or create statefile. Check your config."); error!("{:?}",e); return ExitCode::FAILURE; + } } + }, + Err(e) => { + error!("Could not open or create statefile. Check your config."); + error!("{:?}",e); + return ExitCode::FAILURE; + } }; let statehandle = Arc::new(RwLock::new(state)); @@ -201,7 +203,7 @@ async fn main() -> ExitCode { } //Save state - match statehandle.write().await.write_to_file(&config.statefile) { + match statehandle.write().await.write_to_file(&config.statefile).await { Ok(()) => debug!("Successfully wrote state to {:?}",config.statefile), Err(e) => { error!("Problem while writing to statefile"); diff --git a/src/state.rs b/src/state.rs index 15a82aa..ae32641 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,90 +1,1261 @@ use serde::{Deserialize,Serialize}; use crate::Coordinate; +use thiserror::Error; use tokio::sync::RwLock; -use std::collections::HashMap; +use std::collections::{HashMap,HashSet}; +use std::fs::{self,File}; use std::io; use std::sync::Arc; +use rmp_serde::{decode,encode}; +use fedichat::state::{StatePermissionKey,StatePermissionValue}; +use fedichat::client::ServerError; -// Is there a way to guarantee that creating a state will create a permission -// at the same time in the event of a crash? -#[derive(Serialize,Deserialize)] -pub struct LoadableState { - room_states: HashMap, - perms: HashMap +// These ergonomics kind of suck, might get reworked +// +// The general premise is that the user creates what Operation they want. Then they +// call state.to_unlockable_operation(operation) which returns an UnlockableOperation. +// The only difference to a normal operation is that this one has a list of locks +// that must be unlocked. Each lock contains all possible permissions that could +// unlock it. To unlock the whole operation the user must construct a similar +// list of which relevant keys they posess, a single one for each lock. This +// lets you construct an UnlockedOperation which allows you to actually perform +// the given action on the state. Constructing an UnlockedOperation means that you are +// guaranteed to have the right permissions. +// +// I wonder if its possible to formalize this? Lots of other permission systems have +// done it. +// +// Also another issue is that the operation is not fully atomic. Permission lookup +// and writing/reding are separate operations with different locks. Means that permissions +// can theoretically change between lookup the operation itself. It probably doesn't +// matter a ton but I'm not sure + +// read, write, append, create, or delete +// path is given separately +#[derive(Debug)] +pub enum Operation { + // Maybe writes can get upgraded to creates? + // Can create get downgraded into a write?? + // Create need write permission the the upper level directory but writes only need + // read + Create(fedichat::state::StateValue,fedichat::state::PermissionTable), + Delete, + Append(fedichat::state::StateValue), + Write(fedichat::state::StateValue), + + Read, + //TODO: Implement move, it is going to take two traversals so cutting it for now + //Move(fedichat::state::StatePath), + //Who should have permissions for these? + PermAdd(fedichat::state::StatePermission), + PermRead, + PermDel(fedichat::state::StatePermissionKey), } -impl LoadableState { - pub fn new(room_states: HashMap, perms: HashMap) -> Self { - LoadableState{ - room_states, - perms - } +pub struct StateOperation { + pub op: Operation, + pub path: fedichat::state::StatePath, + pub room: Coordinate, +} +impl StateOperation { + pub fn new(room: Coordinate, path: fedichat::state::StatePath, op: Operation) -> Self { + StateOperation { room, op, path } + } + pub fn to_unlockable(self, locks: Vec>) -> UnlockableOperation { + UnlockableOperation { + op: self.op, + room: self.room, + path: self.path, + locks + } } } +#[derive(Debug)] +pub struct UnlockableOperation { + pub op: Operation, + pub room: Coordinate, + pub path: fedichat::state::StatePath, + // Conjunction of disjunctions + // AND of ORs + // Every iteration of this must be unlocked + locks: Vec> +} + +impl UnlockableOperation { + // One key per lock + pub fn unlock(self, keys: Vec) -> Result { + if keys.len() != self.locks.len() { + return Err(StateError::NotEnoughKeys); + } + // Make sure every lock has a relevant key + for (lock,key) in self.locks.iter().zip(keys.iter()) { + // Server-level permissions work as a key for everything + if key == &StatePermissionKey::Server { + continue + } + + // Operator permissions also act as a universal key for an individual + // room. This may change? I'm suspicious of this + if key == &StatePermissionKey::Operator { + continue + } + + if !lock.contains(key) { + return Err(StateError::MismatchedKey(lock.clone(),key.clone())); + } + + } + + Ok(UnlockedOperation { + op: self.op, + room: self.room, + path: self.path + }) + } + pub fn get_locks(&self) -> &[HashSet] { + &self.locks + } +} + +// Same contents as a StateOperation +// HOWEVER, constructing this type means that the operation can be executed for free +pub struct UnlockedOperation { + pub room: Coordinate, + pub op: Operation, + pub path: fedichat::state::StatePath, +} + + +#[derive(Serialize,Deserialize)] +pub struct LoadableState { + room_states: HashMap, +} +impl LoadableState { + pub fn new(room_states: HashMap) -> Self { + LoadableState{ + room_states, + //perms + } + } +} + + + // This whole thing needs to be RWLock protected to be able to add new rooms +// +// How do I add new users? pub struct State { room_states: HashMap>>, - perms: HashMap>> + //perms: HashMap>> } impl State { pub fn new() -> Self { State { room_states: HashMap::new(), - perms: HashMap::new() + //perms: HashMap::new() + } + } + //TODO: At some point this should take in some way of intializing state. By default + // the state shall be rw for everyone + pub fn create_room(&mut self, coord: Coordinate, op: fedichat::User) -> Result<(),StateError> { + if self.room_states.contains_key(&coord) { + Err(StateError::RoomAlreadyExists(coord)) + } else { + // Generate room from skeleton. Sets up basic dirs and permissions + // TODO still is figuring out how to make the room creator get admin/op + // permissions. Might be able to push this past the demo? + let root = StateNode::gen_root(op); + self.room_states.insert(coord,Arc::new(RwLock::new(root))); + Ok(()) + } + } + + pub fn room_exists(&self, coord: Coordinate) -> bool { + self.room_states.contains_key(&coord) + } + + pub async fn is_joined(&self, coord: Coordinate, user: fedichat::User) -> bool { + match self.get_bakedin(coord.clone(),"members".to_owned()).await { + Ok(val) => val.0.contains_key(&StatePermissionKey::User(user.clone())), + // Dunno if I should hide this error + Err(_e) => false + } + } + + // Get all banned users. If we are not banned, add our name into the joined list + pub async fn join(&self, coord: Coordinate, user: fedichat::User) -> Result<(),StateError> { + let bans = self.get_bakedin(coord.clone(),"banned".to_owned()).await?; + if bans.0.contains_key(&StatePermissionKey::User(user.clone())) { + return Err(StateError::Banned(coord)); + } + + let permission = fedichat::state::StatePermission(StatePermissionKey::User(user),StatePermissionValue::None); + // If we are not banned then do an add permission action to the users list + self.execute_operation(UnlockedOperation { + room: coord.clone(), + path: fedichat::state::StatePath(vec!["room".to_owned(),"members".to_owned()]), + op: Operation::PermAdd(permission) + }).await.map(|_| ()) + + } + + // Get a baked in state value. Useful for checking if a user is joined to a room + pub async fn get_bakedin(&self, coord: Coordinate, key: String) -> Result { + match self.execute_operation(UnlockedOperation { + room: coord.clone(), + path: fedichat::state::StatePath(vec!["room".to_owned(),key.clone()]), + op: Operation::PermRead + }).await ? { + Some(val) => Ok(val.get_perms().clone()), + // This probably isn't the right error + // and it probably isn't even reachable + None => Err(StateError::Unreachable(coord.clone(),fedichat::state::StatePath(vec!["room".to_owned(),key]))) + } + + + } + + pub async fn get_operators(&self, coord: &Coordinate) -> Result { + // This is part of the permission checking so no need to check twice to see + // if the server can actually read the ops file + match self.execute_operation(UnlockedOperation { + room: coord.clone(), + path: fedichat::state::StatePath(vec!["room".to_owned(),"ops".to_owned()]), + op: Operation::PermRead + }).await ? { + Some(val) => Ok(val.get_perms().clone()), + // This probably isn't the right error + // and it probably isn't even reachable + None => Err(StateError::Unreachable(coord.clone(),fedichat::state::StatePath(vec!["room".to_owned(),"ops".to_owned()]))) } } pub fn from_loadable_state(other: LoadableState) -> Self { - unimplemented!() + let mut room_states = HashMap::new(); + //let mut perms = HashMap::new(); + + + for (key,state) in other.room_states { + room_states.insert(key,Arc::new(RwLock::new(state))); + } + + State { + room_states, + //perms + } } - pub fn write_to_file(&mut self, path: &str) -> Result<(),io::Error> { + // Write the entirety of the server state to a file + // NOTE: This maybe could be more efficient if it only partially locked the table + // and wrote out each room's state to a different file. Not sure it matters a ton though + pub async fn write_to_file(&mut self, path: &str) -> Result<(),StateError> { // Should write to state.next file, then move to actual state file // Uses twice as much space but won't corrupt state if the program crashes // during an autosave - unimplemented!() + let state_to_write = self.to_loadable_state().await; + + let mut tmp_fname = path.to_owned(); + tmp_fname.push_str(".tmp"); + let mut tmp_file = File::create(&tmp_fname)?; + + // TODO Serialzeme??? What format oh no + // bincode is dead? could use postcard maybe + encode::write(&mut tmp_file,&state_to_write)?; + + fs::rename(tmp_fname,path)?; + + + Ok(()) } - pub fn load_from_file(path: &str) -> Result { - unimplemented!() + pub fn load_from_file(path: &str) -> Result { + let file = File::open(path)?; + let loadable_state = decode::from_read(file)?; + + Ok(State::from_loadable_state(loadable_state)) } pub async fn to_loadable_state(&self) -> LoadableState { let mut room_states = HashMap::new(); - let mut perms = HashMap::new(); + //let mut perms = HashMap::new(); //NOTE: This clone can be really heavy, I wonder if I can just lock the whole - //state to avoid copying it + //state to avoid copying it. If I wrote each room individually then I wouldn't have to + //duplicate the entire state in memory because I could drop each room as I wrote it for (coordinate,state) in self.room_states.iter() { room_states.insert(coordinate.clone(),state.read().await.clone()); } - for (coordinate,perm) in self.perms.iter() { - perms.insert(coordinate.clone(),perm.read().await.clone()); - } - LoadableState::new(room_states,perms) + //for (coordinate,perm) in self.perms.iter() { + // perms.insert(coordinate.clone(),perm.read().await.clone()); + //} + //LoadableState::new(room_states,perms) + LoadableState::new(room_states) } // This traverses to the right leaf node and gets all permissions for an object - pub async fn get_required_permissions(&self, path: fedichat::state::StatePath) - -> HashMap - { - unimplemented!() + //async fn get_permissions(&self,coord: Coordinate, path: fedichat::state::StatePath) + // -> HashMap + //{ + // unimplemented!() + //} + + //// Determine which users/groups/roles could grant the right permission + //pub async fn get_allowed_keys( + // &self, + // coord: Coordinate, + // path: fedichat::state::StatePath, + // perm: fedichat::state::StatePermissionValue + //) -> HashSet { + // let mut keys = HashSet::new(); + // for (k,v) in self.get_permissions(coord,path).await { + // if v == perm { + // keys.insert(k); + // } + // } + // keys + + //} + + // Is resolving permission a SAT problem??? + pub async fn to_unlockable_operation( + &self, + operation: StateOperation, + ) -> Result { + + let mut locks = Vec::new(); + + let mut path = operation.path.0.clone(); + let room = operation.room.clone(); + + let final_segment: Option = match operation.op { + Operation::Create(_,_) | Operation::Delete => match path.pop() { + Some(val) => Some(val), + None => return Err(StateError::EmptyPath(fedichat::state::StatePath(path))) + }, + _ => None + }; + + + // Traverse over the state path and build the lock vec + // Recursively descend down state tree in a cursed iterative way + let head = match self.room_states.get(&operation.room) { + Some(val) => val.read().await, + None => {return Err(StateError::ValueNotFound(room,fedichat::state::StatePath(path)));} + }; + + let mut cursor: &StateNode = &head; + + // Root implicitly grants read. Kinda a bug kinda a feature + + for segment in path.iter() { + match cursor { + &StateNode::Directory{ref nodes, ref perms} => { + locks.push(perms.get_allowed(&StatePermissionValue::Read)); + cursor = match nodes.get(segment) { + Some(val) => val, + None => return Err(StateError::ValueNotFound(room,fedichat::state::StatePath(path))) + } + }, + // Should never reach as the final segment comes after this + &StateNode::Value{val: ref _val, perms: ref _perms} => { + return Err(StateError::ValueNotFound(room,fedichat::state::StatePath(path))) + }, + } + } + use Operation::*; + + let final_perms = match cursor { + StateNode::Value{perms, val: _val} => perms, + StateNode::Directory{perms, nodes: _nodes} => perms + }; + + // Final check, permissions vary based on the desired operation + match operation.op { + // These both need the directory to have different permissions + // might need to pop perms?? not sure + // Also create needs a bonus check in case it gets downgraded to a write? + // + // Cursor for these two is at the parent directory + Create(_,_) => { + // TODO I wrote this while very stressed check it back over + match cursor { + StateNode::Value{perms: _perms, val: _val} => + return Err(StateError::ValueNotFound(room,fedichat::state::StatePath(path))), + StateNode::Directory{perms, nodes} => { + let final_segment = match final_segment { + Some(val) => val, + None => return Err(StateError::ValueNotFound(room,fedichat::state::StatePath(path))) + }; + // Check if we only need write perms + //if let Some(StateNode::Directory{perms,nodes}) = nodes.get(&final_segment) { + match nodes.get(&final_segment) { + None => { + locks.push(perms.get_allowed(&StatePermissionValue::Write)) + }, + Some(StateNode::Value{val: _val, perms: final_perms}) => { + // Downgrade means we don't actually need write permissions + // on the final directory + locks.push(perms.get_allowed(&StatePermissionValue::Read)); + // Downgrade means we need write permissions on final file + locks.push(final_perms.get_allowed(&StatePermissionValue::Write)); + }, + _ => { + return Err(StateError::ValueNotFound(room,fedichat::state::StatePath(path))) + + } + } + //} + } + } + }, + Delete => { + match cursor { + StateNode::Value{perms: _perms, val: _val} => + return Err(StateError::ValueNotFound(room,fedichat::state::StatePath(path))), + StateNode::Directory{perms, nodes} => { + locks.push(perms.get_allowed(&StatePermissionValue::Write)); + let final_segment = match final_segment { + Some(val) => val, + None => return Err(StateError::ValueNotFound(room,fedichat::state::StatePath(path))) + }; + // Do not delete nonempty directories + if let Some(StateNode::Directory{perms: _perms,nodes}) = nodes.get(&final_segment) { + if nodes.len() > 0 { + return Err(StateError::DeleteNotEmpty(fedichat::state::StatePath(path))); + } + } + } + } + }, + // These only depend on final permissions + Append(_) => { + locks.push(final_perms.get_allowed(&StatePermissionValue::ReadWrite)); + }, + Write(_) => { + locks.push(final_perms.get_allowed(&StatePermissionValue::Write)); + }, + Read | PermRead => { + locks.push(final_perms.get_allowed(&StatePermissionValue::Read)); + }, + PermAdd(_) | PermDel(_) => { + locks.push(final_perms.get_allowed(&StatePermissionValue::Owner)); + }, + } + + Ok(operation.to_unlockable(locks)) } - // Make a wrapper for this that allows the client to request a state value - pub async fn get_state_value(&self, path: fedichat::state::StatePath) - -> fedichat::state::StateValue + // Assume we have the right permissions. Then perform the correct operation + // The construction of an UnlockedOperation = granting permission + pub async fn execute_operation(&self, UnlockedOperation {room, mut path, op}: UnlockedOperation) + -> Result,StateError> { - unimplemented!() + match op { + // Read and write use different locks and types so these have to be different + // code segments + // + // Read and PermRead just filter out different parts of the result, otherwise + // are the same. + Operation::Read | Operation::PermRead => { + // Recursively descend down state tree in a cursed iterative way + let head = match self.room_states.get(&room) { + Some(val) => val.read().await, + None => {return Err(StateError::ValueNotFound(room,path));} + + }; + let mut cursor: &StateNode = &head; + + for segment in path.0.iter() { + match cursor { + &StateNode::Directory{ref nodes, perms: ref _perms} => { + cursor = match nodes.get(segment) { + Some(val) => val, + None => return Err(StateError::ValueNotFound(room,path)) + } + }, + &StateNode::Value{val: ref _val, perms: ref _perms} => { + return Err(StateError::ValueNotFound(room,path)) + }, + } + } + Ok(Some(cursor.clone())) + }, + op => { + // When creating or deleting a node we want to end up with the parent + // Mght be able to get around this with funny pointer math + let final_segment: Option = match op { + Operation::Create(_,_) | Operation::Delete => match path.0.pop() { + Some(val) => Some(val), + None => return Err(StateError::EmptyPath(path)) + }, + _ => None + }; + + // Oh no read and write use different types + let mut head = match self.room_states.get(&room) { + Some(val) => val.write().await, + None => {return Err(StateError::ValueNotFound(room,path));} + + }; + // Recursively descend down state tree in a cursed iterative way + let mut cursor: &mut StateNode = &mut head; + + for segment in path.0.iter() { + match cursor { + &mut StateNode::Directory{ref mut nodes, perms: ref _perms} => { + cursor = match nodes.get_mut(segment) { + Some(val) => val, + None => return Err(StateError::ValueNotFound(room,path)) + } + }, + &mut StateNode::Value{val: ref _val, perms: ref _perms} => { + return Err(StateError::ValueNotFound(room,path)) + }, + } + } + + match op { + Operation::Write(other) => { + match cursor { + StateNode::Directory { nodes: _nodes, perms: _perms } => { + Err(StateError::ValueNotFound(room,path)) + }, + StateNode::Value { val, perms: _perms } => { + *val = other; + Ok(None) + } + } + }, + Operation::Create(other,other_perms) => { + // Cursor should be pointing to a directory + match cursor { + StateNode::Directory { nodes, perms: _perms } => { + match final_segment { + Some(final_segment) => { + match nodes.get_mut(&final_segment) { + Some(StateNode::Value {val, perms: _perms}) => { + *val = other; + // NOTE: Might want to return a warning here? + // Because this is a downgrade to a write + Ok(None) + }, + None => { + nodes.insert(final_segment,StateNode::new(other,other_perms)); + Ok(None) + }, + Some(StateNode::Directory { nodes: _nodes, perms: _perms }) => { + Err(StateError::ValueNotFound(room,path)) + }, + } + }, + None => Err(StateError::ValueNotFound(room,path)) + } + }, + // Maybe the type should give more information that + // everything is matched + StateNode::Value { val: _val, perms: _perms } => + Err(StateError::ValueNotFound(room,path)) + } + }, + + Operation::Append(other) => { + match cursor { + StateNode::Directory { nodes: _nodes, perms: _perms } => { + Err(StateError::ValueNotFound(room,path)) + }, + StateNode::Value { val, perms: _perms } => { + match val.push_other(&other) { + Ok(()) => Ok(None), + Err(()) => Err(StateError::MismatchedTypes(val.clone(),other)) + } + } + } + }, + + // TODO: Error paths are duplicated + Operation::Delete => { + // Cursor should be pointing to a directory + match cursor { + StateNode::Directory { nodes, perms: _perms } => { + match final_segment { + Some(final_segment) => match nodes.remove(&final_segment) { + Some(_) => Ok(None), + None => Err(StateError::ValueNotFound(room,path)) + }, + None => Err(StateError::ValueNotFound(room,path)) + } + }, + StateNode::Value { val: _val, perms: _permts } => + Err(StateError::ValueNotFound(room,path)) + } + + }, + Operation::PermAdd(fedichat::state::StatePermission(key,val)) => { + // Cursor should be pointing to a directory + let final_perms = match cursor { + StateNode::Directory { nodes: _nodes, perms } => &mut perms.0, + StateNode::Value { val: _val, perms} => &mut perms.0 + }; + // Wait this can overwrite someone's permissions? Check to make sure that + // we are not downgrading the last owner of a file + final_perms.insert(key,val); + Ok(None) + + }, + Operation::PermDel(key) => { + // Cursor should be pointing to a directory + let final_perms = match cursor { + StateNode::Directory { nodes: _nodes, perms } => &mut perms.0, + StateNode::Value { val: _val, perms} => &mut perms.0 + }; + + // Make sure we do not downgrade the last owner + if let Some(StatePermissionValue::Owner) = final_perms.get(&key) { + let count_owners = final_perms.values().filter(|val| val == &&StatePermissionValue::Owner).count(); + if count_owners > 1 { + final_perms.remove(&key); + } else { + return Err(StateError::RemoveLastOwner) + } + + //Otherwise we can delete for free + } else { + final_perms.remove(&key); + } + + Ok(None) + + }, + Operation::Read | Operation::PermRead => { + // Should I use the unreachable macro? Is there a way to type system + // this? + Err(StateError::Unreachable(room,path)) + }, + } + + } + + } } } -#[derive(Serialize,Deserialize,Clone)] +#[derive(Serialize,Deserialize,Clone,Debug)] pub enum StateNode { - Directory(HashMap), - Value(fedichat::state::StateValue) + Directory{ + nodes: HashMap, + // Should directories return permissions for their children? + perms: fedichat::state::PermissionTable, + }, + Value{ + // NOTE: the type here can be more specific, we shouldn't allow directories + val: fedichat::state::StateValue, + perms: fedichat::state::PermissionTable + }, + // What do I need server nodes to do + // + // Keep track of joined users + // + // Allow appending users to a ban list, and also removing them + // This maybe could be through an extra ban or unban endpoint + // That then updates a user readable banlist + //ServerNode(ServerNode) +} +impl StateNode { + fn new(value: fedichat::state::StateValue, perms: fedichat::state::PermissionTable) -> Self { + use fedichat::state::StateValue; + match value { + StateValue::String(_) | StateValue::Binary(_) => { + StateNode::Value { + val: value, + perms + } + }, + StateValue::Directory(_) => { + StateNode::Directory { + nodes: HashMap::new(), + perms: perms + } + } + + } + } + + pub fn gen_root(operator: fedichat::User) -> Self { + let server_edit_user_read = fedichat::state::PermissionTable( + HashMap::from([ + (StatePermissionKey::Server,StatePermissionValue::Owner), + (StatePermissionKey::Operator,StatePermissionValue::ReadWrite), + (StatePermissionKey::Everyone,StatePermissionValue::Read), + ]) + ); + let server_edit_user_readwrite = fedichat::state::PermissionTable( + HashMap::from([ + (StatePermissionKey::Server,StatePermissionValue::Owner), + (StatePermissionKey::Everyone,StatePermissionValue::ReadWrite), + ]) + ); + let op_perms = fedichat::state::PermissionTable( + HashMap::from([ + (StatePermissionKey::User(operator.clone()),StatePermissionValue::Owner), + (StatePermissionKey::Everyone,StatePermissionValue::Read), + ]) + ); + let members_perms = fedichat::state::PermissionTable( + HashMap::from([ + (StatePermissionKey::Operator,StatePermissionValue::Owner), + (StatePermissionKey::User(operator.clone()),StatePermissionValue::None), + (StatePermissionKey::Everyone,StatePermissionValue::Read), + ]) + ); + let banned_perms = fedichat::state::PermissionTable( + HashMap::from([ + (StatePermissionKey::Operator,StatePermissionValue::Owner), + (StatePermissionKey::User(operator),StatePermissionValue::None), + (StatePermissionKey::Everyone,StatePermissionValue::Read), + ]) + ); + + // + // Baked-in state + // + let name_node = StateNode::Value { + val: fedichat::state::StateValue::String("Newly Created Room".to_owned()), + perms: server_edit_user_read.clone() + }; + let members_node = StateNode::Value { + val: fedichat::state::StateValue::String("List of Members".to_owned()), + perms: members_perms + }; + let banned_node = StateNode::Value { + val: fedichat::state::StateValue::String("List of Bans".to_owned()), + perms: banned_perms + }; + let ops_node = StateNode::Value { + val: fedichat::state::StateValue::String("Operator Permission File".to_owned()), + perms: op_perms + }; + let room_node = StateNode::Directory { + nodes: HashMap::from([ + ("name".to_owned(),name_node), + ("banned".to_owned(),banned_node), + ("members".to_owned(),members_node), + ("ops".to_owned(),ops_node) + ]), + perms: server_edit_user_read.clone() + }; + + + // + // Custom state + // + + // Mostly exists to stop people from deleting the custom state directory + let readme_node = StateNode::Value { + val: fedichat::state::StateValue::String("This is the directory to create custom state in".to_owned()), + perms: server_edit_user_read.clone() + }; + + let user_node = StateNode::Directory { + nodes: HashMap::from([("readme".to_owned(),readme_node)]), + perms: server_edit_user_readwrite.clone() + }; + + // + // The root + // + + StateNode::Directory { + nodes: HashMap::from([("room".to_owned(),room_node),("user".to_owned(),user_node)]), + perms: server_edit_user_read + } + } + + pub fn get_perms(&self) -> &fedichat::state::PermissionTable{ + match self { + StateNode::Value { + val: _val, + perms + } => perms, + StateNode::Directory { + nodes: _nodes, + perms + } => perms + } + } + fn to_state_value(&self) -> fedichat::state::StateValue { + match self { + // Turn directory into a `ls`-like listing + StateNode::Directory { nodes, perms: _perms } => { + let mut map = HashMap::new(); + for (name,node) in nodes { + let perms = match node { + StateNode::Directory{nodes: _, perms} => perms, + StateNode::Value{val: _, perms} => perms, + }; + map.insert(name.to_string(),perms.clone()); + }; + fedichat::state::StateValue::Directory(map) + }, + StateNode::Value { val, perms: _perms } => val.clone() + } + } +} +//#[derive(Serialize,Deserialize,Clone)] +//pub enum PermNode { +// // Directories also have associated permissions +// Directory(HashMap), +// // Permission table for a value/leaf node +// Permission(PermissionTable) +//} + +#[derive(Error,Debug)] +pub enum StateError { + #[error("IO Error: {0}")] + IOError(#[from] io::Error), + #[error("Encoding Error: {0}")] + EncodingError(#[from] encode::Error), + #[error("Decoding Error: {0}")] + DecodingError(#[from] decode::Error), + #[error("Value not found: {1:?} in {0:?}")] + ValueNotFound(Coordinate,fedichat::state::StatePath), + #[error("Reached unreachable code path while trying to access {1:?} in {0:?}")] + Unreachable(Coordinate,fedichat::state::StatePath), + #[error("Not enough keys")] + NotEnoughKeys, + #[error("Mismatch lock {0:?} and key {1:?}")] + MismatchedKey(HashSet,fedichat::state::StatePermissionKey), + #[error("Banned from {0:?}")] + Banned(Coordinate), + #[error("Empty path {0:?}")] + EmptyPath(fedichat::state::StatePath), + #[error("Mismatched types while appending to file: {0:?} versus {1:?}")] + MismatchedTypes(fedichat::state::StateValue,fedichat::state::StateValue), + #[error("Room at {0:?} already exists")] + RoomAlreadyExists(Coordinate), + #[error("Trying to delete directory that is not empty at {0:?}")] + DeleteNotEmpty(fedichat::state::StatePath), + #[error("Cannot remove last owner on file")] + RemoveLastOwner } -#[derive(Serialize,Deserialize,Clone)] -pub enum PermNode { - Directory(HashMap), - Permission(HashMap) +impl From for ServerError { + fn from(value: StateError) -> ServerError { + // TODO: + // Probably needs a pass, throwing away all this error information feels wrong. + // Maybe this should debug print every time it is called lol + match value { + StateError::RemoveLastOwner => ServerError::RemoveLastOwner, + StateError::DeleteNotEmpty(p) => ServerError::DeleteNotEmpty(p), + StateError::MismatchedTypes(_,_) => ServerError::MismatchedTypes, + StateError::ValueNotFound(_,p) => ServerError::ValueNotFound(p), + _ => ServerError::Generic + + + } + + } +} + +#[cfg(test)] +mod test { + use crate::Coordinate; + use super::{State,Operation,StateOperation,StateNode,StateError}; + use fedichat::state::{StatePermissionKey,StatePermissionValue,StatePath,StateValue}; + use std::collections::{HashMap,HashSet}; + #[tokio::test] + async fn ops_test() { + let coords = Coordinate(vec![42,64]); + let my_user = fedichat::User{name: "root".to_owned(), server: "root.dev".to_owned()}; + let mut state = State::new(); + + state.create_room(coords.clone(),my_user.clone()).unwrap(); + println!("{:?}",state.room_states); + + let operators = state.get_operators(&coords).await; + let operators = operators.unwrap(); + let operators = operators.0.get(&StatePermissionKey::User(my_user)); + + println!(""); + println!("{:?}",operators); + + assert_eq!( + operators, + Some(&StatePermissionValue::Owner) + + ); + } + #[tokio::test] + async fn rw_test() { + let coords = Coordinate(vec![31,32,1]); + let my_user = fedichat::User{name: "root".to_owned(), server: "root.dev".to_owned()}; + let mut state = State::new(); + + let path = StatePath(vec!["user".to_owned(),"my_state".to_owned()]); + + let value = fedichat::state::StateValue::String("This is a test string".to_owned()); + + let perms = fedichat::state::PermissionTable( + HashMap::from([ + (StatePermissionKey::User(my_user.clone()),StatePermissionValue::Owner), + ]) + ); + + state.create_room(coords.clone(),my_user.clone()).unwrap(); + println!("{:?}",state.room_states); + + let operation = StateOperation::new(coords.clone(),path.clone(),Operation::Create(value.clone(),perms)); + + let unlockable = state.to_unlockable_operation(operation).await.unwrap(); + + println!("LOCKS: {:?}",unlockable.get_locks()); + + let unlocked = unlockable.unlock(vec![ + StatePermissionKey::Everyone, + StatePermissionKey::Everyone + ]).unwrap(); + + assert!(state.execute_operation(unlocked).await.unwrap().is_none()); + + let operation_2 = StateOperation::new(coords,path,Operation::Read); + + let unlockable = state.to_unlockable_operation(operation_2).await.unwrap(); + + println!("LOCKS: {:?}",unlockable.get_locks()); + + let unlocked = unlockable.unlock(vec![ + StatePermissionKey::Everyone, + StatePermissionKey::Everyone, + StatePermissionKey::User(my_user.clone())] + ).unwrap(); + + match state.execute_operation(unlocked).await.unwrap() { + Some(StateNode::Value{val, perms: _perms}) => assert_eq!(val,value), + _ => panic!("Unexpected value") + } + + } + #[tokio::test] + async fn del_test() { + let coords = Coordinate(vec![31,32,1]); + let my_user = fedichat::User{name: "root".to_owned(), server: "root.dev".to_owned()}; + let mut state = State::new(); + + let path = StatePath(vec!["user".to_owned(),"my_state".to_owned()]); + + let value = fedichat::state::StateValue::String("This is a test string".to_owned()); + + let perms = fedichat::state::PermissionTable( + HashMap::from([ + (StatePermissionKey::User(my_user.clone()),StatePermissionValue::Owner), + ]) + ); + + state.create_room(coords.clone(),my_user.clone()).unwrap(); + println!("{:?}",state.room_states); + + let operation = StateOperation::new(coords.clone(),path.clone(),Operation::Create(value.clone(),perms)); + + let unlockable = state.to_unlockable_operation(operation).await.unwrap(); + + println!("LOCKS: {:?}",unlockable.get_locks()); + + let unlocked = unlockable.unlock(vec![ + StatePermissionKey::Everyone, + StatePermissionKey::Everyone + ]).unwrap(); + + assert!(state.execute_operation(unlocked).await.unwrap().is_none()); + let operation_2 = StateOperation::new(coords.clone(),path.clone(),Operation::Delete); + + let unlockable = state.to_unlockable_operation(operation_2).await.unwrap(); + + println!("LOCKS: {:?}",unlockable.get_locks()); + + let unlocked = unlockable.unlock(vec![ + StatePermissionKey::Everyone, + StatePermissionKey::Everyone] + ).unwrap(); + + assert!(state.execute_operation(unlocked).await.unwrap().is_none()); + + let operation_3 = StateOperation::new(coords.clone(),path.clone(),Operation::Read); + let unlockable = state.to_unlockable_operation(operation_3).await; + + match unlockable { + // Maybe should match on error type + Err(StateError::ValueNotFound(c,p)) => assert!(c == coords && p == path), + e => panic!("Operation should have failed: {:?}",e), + + } + + } + #[tokio::test] + async fn create_downgrade_test() { + let coords = Coordinate(vec![28,36,-1,2]); + let my_user = fedichat::User{name: "root".to_owned(), server: "root.dev".to_owned()}; + let mut state = State::new(); + + let path = StatePath(vec!["user".to_owned(),"my_state".to_owned()]); + + let value = fedichat::state::StateValue::String("This is a test string".to_owned()); + let value2 = fedichat::state::StateValue::String("This is another test string".to_owned()); + let value3 = fedichat::state::StateValue::String("This is another test string".to_owned()); + let perms = fedichat::state::PermissionTable( + HashMap::from([ + (StatePermissionKey::User(my_user.clone()),StatePermissionValue::Owner), + ]) + ); + + state.create_room(coords.clone(),my_user.clone()).unwrap(); + println!("{:?}",state.room_states); + + let operation = StateOperation::new(coords.clone(),path.clone(),Operation::Create(value.clone(),perms.clone())); + + let unlockable = state.to_unlockable_operation(operation).await.unwrap(); + + println!("LOCKS: {:?}",unlockable.get_locks()); + + let unlocked = unlockable.unlock(vec![ + StatePermissionKey::Everyone, + StatePermissionKey::Everyone + ]).unwrap(); + + assert!(state.execute_operation(unlocked).await.unwrap().is_none()); + + let operation = StateOperation::new(coords.clone(),path.clone(),Operation::Create(value2.clone(),perms)); + + let unlockable = state.to_unlockable_operation(operation).await.unwrap(); + + println!("LOCKS: {:?}",unlockable.get_locks()); + + let unlocked = unlockable.unlock(vec![ + StatePermissionKey::Everyone, + StatePermissionKey::Everyone, + StatePermissionKey::User(my_user.clone()) + ]).unwrap(); + assert!(state.execute_operation(unlocked).await.unwrap().is_none()); + + let operation_2 = StateOperation::new(coords.clone(),path.clone(),Operation::Read); + let unlockable = state.to_unlockable_operation(operation_2).await.unwrap(); + println!("LOCKS: {:?}",unlockable.get_locks()); + let unlocked = unlockable.unlock(vec![ + StatePermissionKey::Everyone, + StatePermissionKey::Everyone, + StatePermissionKey::User(my_user.clone())] + ).unwrap(); + match state.execute_operation(unlocked).await.unwrap() { + Some(StateNode::Value{val, perms: _perms}) => assert_eq!(val,value2), + _ => panic!("Unexpected value") + } + + + let operation = StateOperation::new(coords.clone(),path.clone(),Operation::Write(value3.clone())); + let unlockable = state.to_unlockable_operation(operation).await.unwrap(); + println!("LOCKS: {:?}",unlockable.get_locks()); + let unlocked = unlockable.unlock(vec![ + StatePermissionKey::Everyone, + StatePermissionKey::Everyone, + StatePermissionKey::User(my_user.clone()) + ]).unwrap(); + assert!(state.execute_operation(unlocked).await.unwrap().is_none()); + + let operation_2 = StateOperation::new(coords,path,Operation::Read); + let unlockable = state.to_unlockable_operation(operation_2).await.unwrap(); + println!("LOCKS: {:?}",unlockable.get_locks()); + let unlocked = unlockable.unlock(vec![ + StatePermissionKey::Everyone, + StatePermissionKey::Everyone, + StatePermissionKey::User(my_user.clone())] + ).unwrap(); + match state.execute_operation(unlocked).await.unwrap() { + Some(StateNode::Value{val, perms: _perms}) => assert_eq!(val,value3), + _ => panic!("Unexpected value") + } + + } + #[tokio::test] + async fn append_test() { + let coords = Coordinate(vec![-1,2,1]); + let my_user = fedichat::User{name: "root".to_owned(), server: "root.dev".to_owned()}; + let mut state = State::new(); + + let path = StatePath(vec!["user".to_owned(),"my_state".to_owned()]); + + let value = fedichat::state::StateValue::String("This is a test".to_owned()); + let value2 = fedichat::state::StateValue::String(" string".to_owned()); + + let perms = fedichat::state::PermissionTable( + HashMap::from([ + (StatePermissionKey::User(my_user.clone()),StatePermissionValue::Owner), + ]) + ); + + state.create_room(coords.clone(),my_user.clone()).unwrap(); + println!("{:?}",state.room_states); + + + let operation = StateOperation::new(coords.clone(),path.clone(),Operation::Create(value.clone(),perms)); + let unlockable = state.to_unlockable_operation(operation).await.unwrap(); + println!("LOCKS: {:?}",unlockable.get_locks()); + let unlocked = unlockable.unlock(vec![ + StatePermissionKey::Everyone, + StatePermissionKey::Everyone + ]).unwrap(); + assert!(state.execute_operation(unlocked).await.unwrap().is_none()); + + let operation = StateOperation::new(coords.clone(),path.clone(),Operation::Append(value2.clone())); + let unlockable = state.to_unlockable_operation(operation).await.unwrap(); + println!("LOCKS: {:?}",unlockable.get_locks()); + let unlocked = unlockable.unlock(vec![ + StatePermissionKey::Everyone, + StatePermissionKey::Everyone, + StatePermissionKey::User(my_user.clone()) + ]).unwrap(); + assert!(state.execute_operation(unlocked).await.unwrap().is_none()); + + let operation_2 = StateOperation::new(coords,path,Operation::Read); + let unlockable = state.to_unlockable_operation(operation_2).await.unwrap(); + println!("LOCKS: {:?}",unlockable.get_locks()); + let unlocked = unlockable.unlock(vec![ + StatePermissionKey::Everyone, + StatePermissionKey::Everyone, + StatePermissionKey::User(my_user.clone()) + ]).unwrap(); + match state.execute_operation(unlocked).await.unwrap() { + Some(StateNode::Value{val, perms: _perms}) => assert_eq!(val,StateValue::String("This is a test string".to_owned())), + _ => panic!("Unexpected value") + } + } + // Permission tests + #[tokio::test] + async fn perm_add_test() { + let coords = Coordinate(vec![42,64]); + let my_user = fedichat::User{name: "root".to_owned(), server: "root.dev".to_owned()}; + let other_user = fedichat::User{name: "runt".to_owned(), server: "root.dev".to_owned()}; + let mut state = State::new(); + + let perm = fedichat::state::StatePermission(StatePermissionKey::User(other_user.clone()),StatePermissionValue::ReadWrite); + + let path = StatePath(vec!["room".to_owned(),"ops".to_owned()]); + + state.create_room(coords.clone(),my_user.clone()).unwrap(); + println!("{:?}",state.room_states); + + let operation = StateOperation::new(coords.clone(),path.clone(),Operation::PermAdd(perm)); + let unlockable = state.to_unlockable_operation(operation).await.unwrap(); + println!("LOCKS: {:?}",unlockable.get_locks()); + assert_eq!(unlockable.get_locks()[2], + HashSet::from([StatePermissionKey::User(my_user.clone())])); + let unlocked = unlockable.unlock(vec![ + StatePermissionKey::Everyone, + StatePermissionKey::Everyone, + StatePermissionKey::User(my_user.clone()) + ]).unwrap(); + assert!(state.execute_operation(unlocked).await.unwrap().is_none()); + + + + let operators = state.get_operators(&coords).await; + let operators = operators.unwrap(); + let operators = operators.0.get(&StatePermissionKey::User(other_user)); + + println!(""); + println!("{:?}",operators); + + assert_eq!( + operators, + Some(&StatePermissionValue::ReadWrite) + ); + } + #[tokio::test] + async fn last_owner_test() { + let coords = Coordinate(vec![42,64]); + let my_user = fedichat::User{name: "root".to_owned(), server: "root.dev".to_owned()}; + let mut state = State::new(); + + let path = StatePath(vec!["room".to_owned(),"ops".to_owned()]); + + state.create_room(coords.clone(),my_user.clone()).unwrap(); + println!("{:?}",state.room_states); + + let operation = StateOperation::new(coords.clone(),path.clone(), + Operation::PermDel(StatePermissionKey::User(my_user.clone()))); + let unlockable = state.to_unlockable_operation(operation).await.unwrap(); + println!("LOCKS: {:?}",unlockable.get_locks()); + assert_eq!(unlockable.get_locks()[2], + HashSet::from([StatePermissionKey::User(my_user.clone())])); + let unlocked = unlockable.unlock(vec![ + StatePermissionKey::Everyone, + StatePermissionKey::Everyone, + StatePermissionKey::User(my_user.clone()) + ]).unwrap(); + match state.execute_operation(unlocked).await { + Err(StateError::RemoveLastOwner) => (), + e => panic!("{:?}",e) + } + + + + //let operators = state.get_operators(&coords).await; + //let operators = operators.unwrap(); + //let operators = operators.0.get(&StatePermissionKey::User(other_user)); + + //println!(""); + //println!("{:?}",operators); + + //assert_eq!( + // operators, + // Some(&StatePermissionValue::ReadWrite) + //); + } + #[tokio::test] + async fn perm_del_test() { + let coords = Coordinate(vec![42,64]); + let my_user = fedichat::User{name: "root".to_owned(), server: "root.dev".to_owned()}; + let other_user = fedichat::User{name: "runt".to_owned(), server: "root.dev".to_owned()}; + let mut state = State::new(); + + let perm = fedichat::state::StatePermission(StatePermissionKey::User(other_user.clone()),StatePermissionValue::ReadWrite); + + let path = StatePath(vec!["room".to_owned(),"ops".to_owned()]); + + state.create_room(coords.clone(),my_user.clone()).unwrap(); + println!("{:?}",state.room_states); + + let operation = StateOperation::new(coords.clone(),path.clone(),Operation::PermAdd(perm)); + let unlockable = state.to_unlockable_operation(operation).await.unwrap(); + println!("LOCKS: {:?}",unlockable.get_locks()); + assert_eq!(unlockable.get_locks()[2], + HashSet::from([StatePermissionKey::User(my_user.clone())])); + let unlocked = unlockable.unlock(vec![ + StatePermissionKey::Everyone, + StatePermissionKey::Everyone, + StatePermissionKey::User(my_user.clone()) + ]).unwrap(); + assert!(state.execute_operation(unlocked).await.unwrap().is_none()); + + + let operation = StateOperation::new(coords.clone(),path.clone(), + Operation::PermDel(StatePermissionKey::User(other_user.clone()))); + let unlockable = state.to_unlockable_operation(operation).await.unwrap(); + println!("LOCKS: {:?}",unlockable.get_locks()); + assert_eq!(unlockable.get_locks()[2], + HashSet::from([StatePermissionKey::User(my_user.clone())])); + let unlocked = unlockable.unlock(vec![ + StatePermissionKey::Everyone, + StatePermissionKey::Everyone, + StatePermissionKey::User(my_user.clone()) + ]).unwrap(); + assert!(state.execute_operation(unlocked).await.unwrap().is_none()); + + let operators = state.get_operators(&coords).await; + let operators = operators.unwrap(); + let operators = operators.0.get(&StatePermissionKey::User(other_user)); + + println!(""); + println!("{:?}",operators); + + assert_eq!( + operators, + None + ); + } }