Implement the authentication API

This commit is contained in:
Savanni D'Gerinel 2022-11-17 17:51:16 -05:00
parent 6e1e4d7811
commit 6d32a641b1
3 changed files with 307 additions and 0 deletions

View File

@ -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<Self, Self::Err> {
Ok(SessionToken(s.to_owned()))
}
}
impl From<SessionToken> 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<Self, Self::Err> {
Ok(Invitation(s.to_owned()))
}
}
impl From<Invitation> 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<Self, Self::Err> {
Ok(UserId(s.to_owned()))
}
}
impl From<UserId> for String {
fn from(s: UserId) -> Self {
s.0.clone()
}
}
impl FromSql for UserId {
fn column_result(val: ValueRef<'_>) -> FromSqlResult<Self> {
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<Self, Self::Err> {
Ok(Username(s.to_owned()))
}
}
impl From<Username> for String {
fn from(s: Username) -> Self {
s.0.clone()
}
}
impl FromSql for Username {
fn column_result(val: ValueRef<'_>) -> FromSqlResult<Self> {
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<UserId, AuthenticationError>;
fn create_invitation(&mut self, user: UserId) -> AppResult<Invitation, AuthenticationError>;
fn authenticate(
&mut self,
invitation: Invitation,
) -> AppResult<SessionToken, AuthenticationError>;
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<UserId, AuthenticationError>;
}
#[derive(Default)]
pub struct MemoryAuth {
users: HashMap<Username, UserId>,
inverse_users: HashMap<UserId, Username>,
invitations: HashMap<Invitation, UserId>,
sessions: HashMap<SessionToken, UserId>,
}
impl AuthenticationDB for MemoryAuth {
fn create_user(&mut self, username: Username) -> AppResult<UserId, AuthenticationError> {
#[cfg(test)]
let _ = maybe_fail::<AuthenticationError>(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<Invitation, AuthenticationError> {
#[cfg(test)]
let _ = maybe_fail::<AuthenticationError>(vec![])?;
if !self.inverse_users.contains_key(&user) {
return error::<Invitation, AuthenticationError>(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<SessionToken, AuthenticationError> {
#[cfg(test)]
let _ = maybe_fail::<AuthenticationError>(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::<SessionToken, AuthenticationError>(AuthenticationError::InvalidInvitation)
}
}
fn delete_session(&mut self, session: SessionToken) -> AppResult<(), AuthenticationError> {
#[cfg(test)]
let _ = maybe_fail::<AuthenticationError>(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::<AuthenticationError>(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::<AuthenticationError>(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::<HashMap<Invitation, UserId>>();
self.sessions = self
.sessions
.iter()
.filter(|(_, value)| **value != user)
.map(|(key, value)| (key.clone(), value.clone()))
.collect::<HashMap<SessionToken, UserId>>();
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<UserId, AuthenticationError> {
Ok(self
.users
.get(&username)
.map(|u| u.clone())
.ok_or(AuthenticationError::UserNotFound))
}
}
#[cfg(test)]
mod test {}

31
server/src/errors.rs Normal file
View File

@ -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<A, E> = Result<Result<A, E>, FatalError>;
pub fn ok<A, E>(val: A) -> AppResult<A, E> {
Ok(Ok(val))
}
pub fn error<A, E>(err: E) -> AppResult<A, E> {
Ok(Err(err))
}
pub fn fatal<A, E>(err: FatalError) -> AppResult<A, E> {
Err(err)
}
#[cfg(test)]
pub fn maybe_fail<E>(possible_errors: Vec<E>) -> AppResult<(), E> {
ok(())
}

View File

@ -1,7 +1,9 @@
use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use warp::Filter; use warp::Filter;
mod authentication;
mod database; mod database;
mod errors;
#[tokio::main] #[tokio::main]
pub async fn main() { pub async fn main() {