refactor auth logic into a common module (#54)

Reviewed-on: lavina/lavina#54
This commit is contained in:
Nikita Vilunov 2024-04-23 10:10:10 +00:00
parent 6c08d69f41
commit d805061d5b
6 changed files with 82 additions and 48 deletions

View File

@ -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<Verdict> {
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<UpdatePasswordResult> {
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)
}
}

View File

@ -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;

View File

@ -38,7 +38,7 @@ impl Storage {
Ok(Storage { conn })
}
pub async fn retrieve_user_by_name(&mut self, name: &str) -> Result<Option<StoredUser>> {
pub async fn retrieve_user_by_name(&self, name: &str) -> Result<Option<StoredUser>> {
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<Option<()>> {
pub async fn set_password<'a>(&'a self, name: &'a str, pwd: &'a str) -> Result<Option<()>> {
async fn inner(txn: &mut Transaction<'_, Sqlite>, name: &str, pwd: &str) -> Result<Option<()>> {
let id: Option<(u32,)> = sqlx::query_as("select * from users where name = ? limit 1;")
.bind(name)

View File

@ -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 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")),
}
};
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"));
}
Ok(())
}
async fn handle_registered_socket<'a>(

View File

@ -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()),

View File

@ -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,7 +142,10 @@ 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 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",
@ -150,8 +154,8 @@ async fn endpoint_set_password(
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::<Bytes>::default());
*response.status_mut() = StatusCode::NO_CONTENT;
Ok(response)