diff --git a/server/src/authentication.rs b/server/src/authentication.rs new file mode 100644 index 0000000..7ed1f45 --- /dev/null +++ b/server/src/authentication.rs @@ -0,0 +1,274 @@ +use crate::errors::{error, fatal, ok, AppResult}; +use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ValueRef}; +use std::collections::HashMap; +use std::{convert::Infallible, str::FromStr}; +use thiserror::Error; +use uuid::{adapter::Hyphenated, Uuid}; + +#[cfg(test)] +use crate::errors::maybe_fail; + +#[derive(Debug, Error)] +pub enum AuthenticationError { + #[error("username already exists")] + DuplicateUsername, + #[error("invitation is not valid")] + InvalidInvitation, + #[error("session token not found")] + InvalidSession, + #[error("user not found")] + UserNotFound, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct SessionToken(String); + +impl From<&str> for SessionToken { + fn from(s: &str) -> Self { + SessionToken(s.to_owned()) + } +} + +impl FromStr for SessionToken { + type Err = Infallible; + + fn from_str(s: &str) -> Result { + Ok(SessionToken(s.to_owned())) + } +} + +impl From for String { + fn from(s: SessionToken) -> Self { + s.0.clone() + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Invitation(String); + +impl From<&str> for Invitation { + fn from(s: &str) -> Self { + Invitation(s.to_owned()) + } +} + +impl FromStr for Invitation { + type Err = Infallible; + + fn from_str(s: &str) -> Result { + Ok(Invitation(s.to_owned())) + } +} + +impl From for String { + fn from(s: Invitation) -> Self { + s.0.clone() + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct UserId(String); + +impl From<&str> for UserId { + fn from(s: &str) -> Self { + UserId(s.to_owned()) + } +} + +impl FromStr for UserId { + type Err = Infallible; + + fn from_str(s: &str) -> Result { + Ok(UserId(s.to_owned())) + } +} + +impl From for String { + fn from(s: UserId) -> Self { + s.0.clone() + } +} + +impl FromSql for UserId { + fn column_result(val: ValueRef<'_>) -> FromSqlResult { + match val { + ValueRef::Text(t) => Ok(UserId::from( + String::from_utf8(Vec::from(t)).unwrap().as_ref(), + )), + _ => Err(FromSqlError::InvalidType), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Username(String); + +impl From<&str> for Username { + fn from(s: &str) -> Self { + Username(s.to_owned()) + } +} + +impl FromStr for Username { + type Err = Infallible; + + fn from_str(s: &str) -> Result { + Ok(Username(s.to_owned())) + } +} + +impl From for String { + fn from(s: Username) -> Self { + s.0.clone() + } +} + +impl FromSql for Username { + fn column_result(val: ValueRef<'_>) -> FromSqlResult { + match val { + ValueRef::Text(t) => Ok(Username::from( + String::from_utf8(Vec::from(t)).unwrap().as_ref(), + )), + _ => Err(FromSqlError::InvalidType), + } + } +} + +pub trait AuthenticationDB { + fn create_user(&mut self, username: Username) -> AppResult; + + fn create_invitation(&mut self, user: UserId) -> AppResult; + + fn authenticate( + &mut self, + invitation: Invitation, + ) -> AppResult; + + fn delete_session(&mut self, session: SessionToken) -> AppResult<(), AuthenticationError>; + + fn delete_invitation(&mut self, invitation: Invitation) -> AppResult<(), AuthenticationError>; + + fn delete_user(&mut self, user: UserId) -> AppResult<(), AuthenticationError>; + + fn validate_session(&self, session: SessionToken) -> AppResult<(), AuthenticationError>; + + fn get_user_id(&self, username: Username) -> AppResult; +} + +#[derive(Default)] +pub struct MemoryAuth { + users: HashMap, + inverse_users: HashMap, + invitations: HashMap, + sessions: HashMap, +} + +impl AuthenticationDB for MemoryAuth { + fn create_user(&mut self, username: Username) -> AppResult { + #[cfg(test)] + let _ = maybe_fail::(vec![])?; + + let userid = UserId::from(format!("{}", Hyphenated::from_uuid(Uuid::new_v4())).as_str()); + self.users.insert(username.clone(), userid.clone()); + self.inverse_users.insert(userid.clone(), username); + + ok(userid) + } + + fn create_invitation(&mut self, user: UserId) -> AppResult { + #[cfg(test)] + let _ = maybe_fail::(vec![])?; + + if !self.inverse_users.contains_key(&user) { + return error::(AuthenticationError::UserNotFound); + } + + let invitation = + Invitation::from(format!("{}", Hyphenated::from_uuid(Uuid::new_v4())).as_str()); + self.invitations.insert(invitation.clone(), user); + ok(invitation) + } + + fn authenticate( + &mut self, + invitation: Invitation, + ) -> AppResult { + #[cfg(test)] + let _ = maybe_fail::(vec![])?; + + if let Some(user) = self.invitations.get(&invitation) { + let session_token = + SessionToken::from(format!("{}", Hyphenated::from_uuid(Uuid::new_v4())).as_str()); + self.sessions.insert(session_token.clone(), user.clone()); + let _ = self.invitations.remove(&invitation); + ok(session_token) + } else { + error::(AuthenticationError::InvalidInvitation) + } + } + + fn delete_session(&mut self, session: SessionToken) -> AppResult<(), AuthenticationError> { + #[cfg(test)] + let _ = maybe_fail::(vec![])?; + + if let Some(_) = self.sessions.remove(&session) { + ok(()) + } else { + error::<(), AuthenticationError>(AuthenticationError::InvalidSession) + } + } + + fn delete_invitation(&mut self, invitation: Invitation) -> AppResult<(), AuthenticationError> { + #[cfg(test)] + let _ = maybe_fail::(vec![])?; + + if let Some(_) = self.invitations.remove(&invitation) { + ok(()) + } else { + error::<(), AuthenticationError>(AuthenticationError::InvalidInvitation) + } + } + + fn delete_user(&mut self, user: UserId) -> AppResult<(), AuthenticationError> { + #[cfg(test)] + let _ = maybe_fail::(vec![])?; + + if let Some(username) = self.inverse_users.remove(&user) { + let _ = self.users.remove(&username); + self.invitations = self + .invitations + .iter() + .filter(|(_, value)| **value != user) + .map(|(key, value)| (key.clone(), value.clone())) + .collect::>(); + self.sessions = self + .sessions + .iter() + .filter(|(_, value)| **value != user) + .map(|(key, value)| (key.clone(), value.clone())) + .collect::>(); + ok(()) + } else { + error::<(), AuthenticationError>(AuthenticationError::UserNotFound) + } + } + + fn validate_session(&self, session: SessionToken) -> AppResult<(), AuthenticationError> { + if self.sessions.contains_key(&session) { + ok(()) + } else { + error::<(), AuthenticationError>(AuthenticationError::InvalidSession) + } + } + + fn get_user_id(&self, username: Username) -> AppResult { + Ok(self + .users + .get(&username) + .map(|u| u.clone()) + .ok_or(AuthenticationError::UserNotFound)) + } +} + +#[cfg(test)] +mod test {} diff --git a/server/src/errors.rs b/server/src/errors.rs new file mode 100644 index 0000000..6a155de --- /dev/null +++ b/server/src/errors.rs @@ -0,0 +1,31 @@ +use thiserror::Error; + +/// This struct covers *fatal* errors for the application. Cross-functional things like full disks, +/// failing disks, database deadlocks, and so forth, which require that the whole application shut +/// down and that the administrator fix a problem. +#[derive(Debug, Error)] +pub enum FatalError { + #[error("disk is full")] + DiskFull, + #[error("io error: {0}")] + Io(std::io::Error), +} + +pub type AppResult = Result, FatalError>; + +pub fn ok(val: A) -> AppResult { + Ok(Ok(val)) +} + +pub fn error(err: E) -> AppResult { + Ok(Err(err)) +} + +pub fn fatal(err: FatalError) -> AppResult { + Err(err) +} + +#[cfg(test)] +pub fn maybe_fail(possible_errors: Vec) -> AppResult<(), E> { + ok(()) +} diff --git a/server/src/main.rs b/server/src/main.rs index 95b8075..50e1927 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,7 +1,9 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use warp::Filter; +mod authentication; mod database; +mod errors; #[tokio::main] pub async fn main() {