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 + ); + } }