Implement the authentication API
This commit is contained in:
parent
6e1e4d7811
commit
6d32a641b1
|
@ -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 {}
|
|
@ -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(())
|
||||
}
|
|
@ -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() {
|
||||
|
|
Loading…
Reference in New Issue