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 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() {
|
||||||
|
|
Loading…
Reference in New Issue