forked from lavina/lavina
refactor auth logic into a common module (#54)
Reviewed-on: lavina/lavina#54
This commit is contained in:
parent
6c08d69f41
commit
d805061d5b
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ use crate::player::PlayerRegistry;
|
||||||
use crate::repo::Storage;
|
use crate::repo::Storage;
|
||||||
use crate::room::RoomRegistry;
|
use crate::room::RoomRegistry;
|
||||||
|
|
||||||
|
pub mod auth;
|
||||||
pub mod player;
|
pub mod player;
|
||||||
pub mod prelude;
|
pub mod prelude;
|
||||||
pub mod repo;
|
pub mod repo;
|
||||||
|
|
|
@ -38,7 +38,7 @@ impl Storage {
|
||||||
Ok(Storage { conn })
|
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 mut executor = self.conn.lock().await;
|
||||||
let res = sqlx::query_as(
|
let res = sqlx::query_as(
|
||||||
"select u.id, u.name, c.password
|
"select u.id, u.name, c.password
|
||||||
|
@ -136,7 +136,7 @@ impl Storage {
|
||||||
Ok(())
|
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<()>> {
|
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;")
|
let id: Option<(u32,)> = sqlx::query_as("select * from users where name = ? limit 1;")
|
||||||
.bind(name)
|
.bind(name)
|
||||||
|
|
|
@ -14,6 +14,7 @@ use tokio::net::tcp::{ReadHalf, WriteHalf};
|
||||||
use tokio::net::{TcpListener, TcpStream};
|
use tokio::net::{TcpListener, TcpStream};
|
||||||
use tokio::sync::mpsc::channel;
|
use tokio::sync::mpsc::channel;
|
||||||
|
|
||||||
|
use lavina_core::auth::{Authenticator, Verdict};
|
||||||
use lavina_core::player::*;
|
use lavina_core::player::*;
|
||||||
use lavina_core::prelude::*;
|
use lavina_core::prelude::*;
|
||||||
use lavina_core::repo::Storage;
|
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<()> {
|
async fn auth_user(storage: &mut Storage, login: &str, plain_password: &str) -> Result<()> {
|
||||||
let stored_user = storage.retrieve_user_by_name(login).await?;
|
let verdict = Authenticator::new(storage).authenticate(login, plain_password).await?;
|
||||||
|
// TODO properly map these onto protocol messages
|
||||||
let stored_user = match stored_user {
|
match verdict {
|
||||||
Some(u) => u,
|
Verdict::Authenticated => Ok(()),
|
||||||
None => {
|
Verdict::UserNotFound => Err(anyhow!("no user found")),
|
||||||
log::info!("User '{}' not found", login);
|
Verdict::InvalidPassword => Err(anyhow!("incorrect credentials")),
|
||||||
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"));
|
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_registered_socket<'a>(
|
async fn handle_registered_socket<'a>(
|
||||||
|
|
|
@ -9,6 +9,7 @@ use std::net::SocketAddr;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::anyhow;
|
||||||
use futures_util::future::join_all;
|
use futures_util::future::join_all;
|
||||||
use prometheus::Registry as MetricsRegistry;
|
use prometheus::Registry as MetricsRegistry;
|
||||||
use quick_xml::events::{BytesDecl, Event};
|
use quick_xml::events::{BytesDecl, Event};
|
||||||
|
@ -21,6 +22,7 @@ use tokio::sync::mpsc::channel;
|
||||||
use tokio_rustls::rustls::{Certificate, PrivateKey};
|
use tokio_rustls::rustls::{Certificate, PrivateKey};
|
||||||
use tokio_rustls::TlsAcceptor;
|
use tokio_rustls::TlsAcceptor;
|
||||||
|
|
||||||
|
use lavina_core::auth::{Authenticator, Verdict};
|
||||||
use lavina_core::player::{PlayerConnection, PlayerId, PlayerRegistry};
|
use lavina_core::player::{PlayerConnection, PlayerId, PlayerRegistry};
|
||||||
use lavina_core::prelude::*;
|
use lavina_core::prelude::*;
|
||||||
use lavina_core::repo::Storage;
|
use lavina_core::repo::Storage;
|
||||||
|
@ -300,28 +302,18 @@ async fn socket_auth(
|
||||||
match AuthBody::from_str(&auth.body) {
|
match AuthBody::from_str(&auth.body) {
|
||||||
Ok(logopass) => {
|
Ok(logopass) => {
|
||||||
let name = &logopass.login;
|
let name = &logopass.login;
|
||||||
let stored_user = storage.retrieve_user_by_name(name).await?;
|
let verdict = Authenticator::new(storage).authenticate(name, &logopass.password).await?;
|
||||||
|
|
||||||
let stored_user = match stored_user {
|
|
||||||
Some(u) => u,
|
|
||||||
None => {
|
|
||||||
log::info!("User '{}' not found", name);
|
|
||||||
return Err(fail("no user found"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// TODO return proper XML errors to the client
|
// TODO return proper XML errors to the client
|
||||||
|
match verdict {
|
||||||
if stored_user.password.is_none() {
|
Verdict::Authenticated => {}
|
||||||
log::info!("Password not defined for user '{}'", name);
|
Verdict::UserNotFound => {
|
||||||
return Err(fail("password is not defined"));
|
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();
|
let name: Str = name.as_str().into();
|
||||||
|
|
||||||
Ok(Authenticated {
|
Ok(Authenticated {
|
||||||
player_id: PlayerId::from(name.clone())?,
|
player_id: PlayerId::from(name.clone())?,
|
||||||
xmpp_name: Name(name.clone()),
|
xmpp_name: Name(name.clone()),
|
||||||
|
|
24
src/http.rs
24
src/http.rs
|
@ -12,6 +12,7 @@ use prometheus::{Encoder, Registry as MetricsRegistry, TextEncoder};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
|
|
||||||
|
use lavina_core::auth::{Authenticator, UpdatePasswordResult};
|
||||||
use lavina_core::prelude::*;
|
use lavina_core::prelude::*;
|
||||||
use lavina_core::repo::Storage;
|
use lavina_core::repo::Storage;
|
||||||
use lavina_core::room::RoomRegistry;
|
use lavina_core::room::RoomRegistry;
|
||||||
|
@ -141,17 +142,20 @@ async fn endpoint_set_password(
|
||||||
*response.status_mut() = StatusCode::BAD_REQUEST;
|
*response.status_mut() = StatusCode::BAD_REQUEST;
|
||||||
return Ok(response);
|
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?;
|
||||||
let payload = ErrorResponse {
|
match verdict {
|
||||||
code: errors::PLAYER_NOT_FOUND,
|
UpdatePasswordResult::PasswordUpdated => {}
|
||||||
message: "No such player exists",
|
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::<Bytes>::default());
|
let mut response = Response::new(Full::<Bytes>::default());
|
||||||
*response.status_mut() = StatusCode::NO_CONTENT;
|
*response.status_mut() = StatusCode::NO_CONTENT;
|
||||||
Ok(response)
|
Ok(response)
|
||||||
|
|
Loading…
Reference in New Issue