From d805061d5bb3fb59e04fbbcf96c4f68460c3de5a Mon Sep 17 00:00:00 2001 From: Nikita Vilunov Date: Tue, 23 Apr 2024 10:10:10 +0000 Subject: [PATCH] refactor auth logic into a common module (#54) Reviewed-on: https://git.vilunov.me/lavina/lavina/pulls/54 --- crates/lavina-core/src/auth.rs | 47 ++++++++++++++++++++++++++++++ crates/lavina-core/src/lib.rs | 1 + crates/lavina-core/src/repo/mod.rs | 4 +-- crates/projection-irc/src/lib.rs | 24 +++++---------- crates/projection-xmpp/src/lib.rs | 30 +++++++------------ src/http.rs | 24 ++++++++------- 6 files changed, 82 insertions(+), 48 deletions(-) create mode 100644 crates/lavina-core/src/auth.rs diff --git a/crates/lavina-core/src/auth.rs b/crates/lavina-core/src/auth.rs new file mode 100644 index 0000000..ba465db --- /dev/null +++ b/crates/lavina-core/src/auth.rs @@ -0,0 +1,47 @@ +use anyhow::Result; + +use crate::prelude::log; +use crate::repo::Storage; + +pub enum Verdict { + Authenticated, + UserNotFound, + InvalidPassword, +} + +pub enum UpdatePasswordResult { + PasswordUpdated, + UserNotFound, +} + +pub struct Authenticator<'a> { + storage: &'a Storage, +} +impl<'a> Authenticator<'a> { + pub fn new(storage: &'a Storage) -> Self { + Self { storage } + } + + pub async fn authenticate(&self, login: &str, provided_password: &str) -> Result { + let Some(stored_user) = self.storage.retrieve_user_by_name(login).await? else { + return Ok(Verdict::UserNotFound); + }; + let Some(expected_password) = stored_user.password else { + log::debug!("Password not defined for user '{}'", login); + return Ok(Verdict::InvalidPassword); + }; + if expected_password == provided_password { + return Ok(Verdict::Authenticated); + } + Ok(Verdict::InvalidPassword) + } + + pub async fn set_password(&self, login: &str, provided_password: &str) -> Result { + let Some(_) = self.storage.retrieve_user_by_name(login).await? else { + return Ok(UpdatePasswordResult::UserNotFound); + }; + self.storage.set_password(login, provided_password).await?; + log::info!("Password changed for player {login}"); + Ok(UpdatePasswordResult::PasswordUpdated) + } +} diff --git a/crates/lavina-core/src/lib.rs b/crates/lavina-core/src/lib.rs index ff52363..e611a01 100644 --- a/crates/lavina-core/src/lib.rs +++ b/crates/lavina-core/src/lib.rs @@ -6,6 +6,7 @@ use crate::player::PlayerRegistry; use crate::repo::Storage; use crate::room::RoomRegistry; +pub mod auth; pub mod player; pub mod prelude; pub mod repo; diff --git a/crates/lavina-core/src/repo/mod.rs b/crates/lavina-core/src/repo/mod.rs index 645c764..714b8cd 100644 --- a/crates/lavina-core/src/repo/mod.rs +++ b/crates/lavina-core/src/repo/mod.rs @@ -38,7 +38,7 @@ impl Storage { Ok(Storage { conn }) } - pub async fn retrieve_user_by_name(&mut self, name: &str) -> Result> { + pub async fn retrieve_user_by_name(&self, name: &str) -> Result> { let mut executor = self.conn.lock().await; let res = sqlx::query_as( "select u.id, u.name, c.password @@ -136,7 +136,7 @@ impl Storage { Ok(()) } - pub async fn set_password<'a>(&'a mut self, name: &'a str, pwd: &'a str) -> Result> { + pub async fn set_password<'a>(&'a self, name: &'a str, pwd: &'a str) -> Result> { async fn inner(txn: &mut Transaction<'_, Sqlite>, name: &str, pwd: &str) -> Result> { let id: Option<(u32,)> = sqlx::query_as("select * from users where name = ? limit 1;") .bind(name) diff --git a/crates/projection-irc/src/lib.rs b/crates/projection-irc/src/lib.rs index ce450c1..278d456 100644 --- a/crates/projection-irc/src/lib.rs +++ b/crates/projection-irc/src/lib.rs @@ -14,6 +14,7 @@ use tokio::net::tcp::{ReadHalf, WriteHalf}; use tokio::net::{TcpListener, TcpStream}; use tokio::sync::mpsc::channel; +use lavina_core::auth::{Authenticator, Verdict}; use lavina_core::player::*; use lavina_core::prelude::*; use lavina_core::repo::Storage; @@ -405,24 +406,13 @@ fn sasl_fail_message(sender: Str, nick: Str, text: Str) -> ServerMessage { } async fn auth_user(storage: &mut Storage, login: &str, plain_password: &str) -> Result<()> { - let stored_user = storage.retrieve_user_by_name(login).await?; - - let stored_user = match stored_user { - Some(u) => u, - None => { - log::info!("User '{}' not found", login); - return Err(anyhow!("no user found")); - } - }; - let Some(expected_password) = stored_user.password else { - log::info!("Password not defined for user '{}'", login); - return Err(anyhow!("password is not defined")); - }; - if expected_password != plain_password { - log::info!("Incorrect password supplied for user '{}'", login); - return Err(anyhow!("passwords do not match")); + let verdict = Authenticator::new(storage).authenticate(login, plain_password).await?; + // TODO properly map these onto protocol messages + match verdict { + Verdict::Authenticated => Ok(()), + Verdict::UserNotFound => Err(anyhow!("no user found")), + Verdict::InvalidPassword => Err(anyhow!("incorrect credentials")), } - Ok(()) } async fn handle_registered_socket<'a>( diff --git a/crates/projection-xmpp/src/lib.rs b/crates/projection-xmpp/src/lib.rs index 6539254..01f0171 100644 --- a/crates/projection-xmpp/src/lib.rs +++ b/crates/projection-xmpp/src/lib.rs @@ -9,6 +9,7 @@ use std::net::SocketAddr; use std::path::PathBuf; use std::sync::Arc; +use anyhow::anyhow; use futures_util::future::join_all; use prometheus::Registry as MetricsRegistry; use quick_xml::events::{BytesDecl, Event}; @@ -21,6 +22,7 @@ use tokio::sync::mpsc::channel; use tokio_rustls::rustls::{Certificate, PrivateKey}; use tokio_rustls::TlsAcceptor; +use lavina_core::auth::{Authenticator, Verdict}; use lavina_core::player::{PlayerConnection, PlayerId, PlayerRegistry}; use lavina_core::prelude::*; use lavina_core::repo::Storage; @@ -300,28 +302,18 @@ async fn socket_auth( match AuthBody::from_str(&auth.body) { Ok(logopass) => { let name = &logopass.login; - let stored_user = storage.retrieve_user_by_name(name).await?; - - let stored_user = match stored_user { - Some(u) => u, - None => { - log::info!("User '{}' not found", name); - return Err(fail("no user found")); - } - }; + let verdict = Authenticator::new(storage).authenticate(name, &logopass.password).await?; // TODO return proper XML errors to the client - - if stored_user.password.is_none() { - log::info!("Password not defined for user '{}'", name); - return Err(fail("password is not defined")); + match verdict { + Verdict::Authenticated => {} + Verdict::UserNotFound => { + return Err(anyhow!("no user found")); + } + Verdict::InvalidPassword => { + return Err(anyhow!("incorrect credentials")); + } } - if stored_user.password.as_deref() != Some(&logopass.password) { - log::info!("Incorrect password supplied for user '{}'", name); - return Err(fail("passwords do not match")); - } - let name: Str = name.as_str().into(); - Ok(Authenticated { player_id: PlayerId::from(name.clone())?, xmpp_name: Name(name.clone()), diff --git a/src/http.rs b/src/http.rs index 302bf5f..4bf3ffe 100644 --- a/src/http.rs +++ b/src/http.rs @@ -12,6 +12,7 @@ use prometheus::{Encoder, Registry as MetricsRegistry, TextEncoder}; use serde::{Deserialize, Serialize}; use tokio::net::TcpListener; +use lavina_core::auth::{Authenticator, UpdatePasswordResult}; use lavina_core::prelude::*; use lavina_core::repo::Storage; use lavina_core::room::RoomRegistry; @@ -141,17 +142,20 @@ async fn endpoint_set_password( *response.status_mut() = StatusCode::BAD_REQUEST; return Ok(response); }; - let Some(_) = storage.set_password(&res.player_name, &res.password).await? else { - let payload = ErrorResponse { - code: errors::PLAYER_NOT_FOUND, - message: "No such player exists", + let verdict = Authenticator::new(&storage).set_password(&res.player_name, &res.password).await?; + match verdict { + UpdatePasswordResult::PasswordUpdated => {} + UpdatePasswordResult::UserNotFound => { + let payload = ErrorResponse { + code: errors::PLAYER_NOT_FOUND, + message: "No such player exists", + } + .to_body(); + let mut response = Response::new(payload); + *response.status_mut() = StatusCode::UNPROCESSABLE_ENTITY; + return Ok(response); } - .to_body(); - let mut response = Response::new(payload); - *response.status_mut() = StatusCode::UNPROCESSABLE_ENTITY; - return Ok(response); - }; - log::info!("Password changed for player {}", res.player_name); + } let mut response = Response::new(Full::::default()); *response.status_mut() = StatusCode::NO_CONTENT; Ok(response)