Compare commits
No commits in common. "76214bc79060e72b2e7a594f95fb02871d36f3c1" and "a84d4fd869787467d2284f23343244566bee1220" have entirely different histories.
76214bc790
...
a84d4fd869
|
@ -101,12 +101,6 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayref"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544"
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.5.2"
|
||||
|
@ -204,17 +198,6 @@ dependencies = [
|
|||
"wyz",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blake2b_simd"
|
||||
version = "0.5.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"arrayvec",
|
||||
"constant_time_eq",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.9.0"
|
||||
|
@ -330,12 +313,6 @@ version = "0.4.8"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f92cfa0fd5690b3cf8c1ef2cabbd9b7ef22fa53cf5e1f92b05103f6d5d1cf6e7"
|
||||
|
||||
[[package]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.1"
|
||||
|
@ -2048,18 +2025,6 @@ dependencies = [
|
|||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-argon2"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"blake2b_simd",
|
||||
"constant_time_eq",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.2.3"
|
||||
|
@ -2467,15 +2432,6 @@ dependencies = [
|
|||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "substring"
|
||||
version = "1.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42ee6433ecef213b2e72f587ef64a2f5943e7cd16fbd82dbe8bc07486c534c86"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.4.0"
|
||||
|
@ -2554,10 +2510,8 @@ dependencies = [
|
|||
"phf",
|
||||
"rand 0.8.3",
|
||||
"refinery",
|
||||
"rust-argon2",
|
||||
"serde",
|
||||
"sqlx",
|
||||
"substring",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
|
|
|
@ -16,7 +16,6 @@ tracing-subscriber = "0.2"
|
|||
toml = "0.5"
|
||||
nom = "5"
|
||||
rand = "0.8"
|
||||
rust-argon2 = "0.8"
|
||||
thiserror = "1.0"
|
||||
itertools = "0.10"
|
||||
async-trait = "0.1"
|
||||
|
@ -31,7 +30,6 @@ matrix-sdk = { git = "https://github.com/matrix-org/matrix-rust-sdk", branch = "
|
|||
refinery = { version = "0.5", features = ["rusqlite"]}
|
||||
barrel = { version = "0.6", features = ["sqlite3"] }
|
||||
tempfile = "3"
|
||||
substring = "1.4"
|
||||
|
||||
[dependencies.sqlx]
|
||||
version = "0.5"
|
||||
|
|
|
@ -60,30 +60,6 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"64e137107139c56a43f7041db933671c210df4fa5110fe481d191fd63b2d3aeb": {
|
||||
"query": "SELECT user_id, password FROM accounts\n WHERE user_id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "user_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "password",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"711d222911c1258365a6a0de1fe00eeec4686fd3589e976e225ad599e7cfc75d": {
|
||||
"query": "SELECT count(*) as \"count: i32\" FROM user_variables\n WHERE room_id = ? and user_id = ?",
|
||||
"describe": {
|
||||
|
|
|
@ -4,6 +4,7 @@ use crate::db::sqlite::Database;
|
|||
use crate::error::BotError;
|
||||
use crate::matrix;
|
||||
use futures::stream::{self, StreamExt};
|
||||
use log::{error, info};
|
||||
use matrix_sdk::{self, identifiers::EventId, room::Joined, Client};
|
||||
use std::clone::Clone;
|
||||
|
||||
|
@ -16,6 +17,13 @@ pub(super) async fn handle_single_result(
|
|||
room: &Joined,
|
||||
event_id: EventId,
|
||||
) {
|
||||
if cmd_result.is_err() {
|
||||
error!(
|
||||
"Command execution error: {}",
|
||||
cmd_result.as_ref().err().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
let html = cmd_result.message_html(respond_to);
|
||||
matrix::send_message(client, room.room_id(), &html, Some(event_id)).await;
|
||||
}
|
||||
|
@ -41,6 +49,10 @@ pub(super) async fn handle_multiple_results(
|
|||
})
|
||||
.collect();
|
||||
|
||||
for result in errors.iter() {
|
||||
error!("Command execution error: '{}' - {}", result.0, result.1);
|
||||
}
|
||||
|
||||
let message = if errors.len() == 0 {
|
||||
format!("{}: Executed {} commands", respond_to, results.len())
|
||||
} else {
|
||||
|
@ -97,6 +109,10 @@ pub(super) async fn execute(
|
|||
Err(e) => (command.to_owned(), Err(ExecutionError(e))),
|
||||
Ok(ctx) => {
|
||||
let cmd_result = execute_command(&ctx).await;
|
||||
info!(
|
||||
"[{}] {} executed: {}",
|
||||
ctx.room.display_name, sender, command
|
||||
);
|
||||
(command.to_owned(), cmd_result)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
use super::{Command, Execution, ExecutionResult};
|
||||
use crate::context::Context;
|
||||
use crate::db::Users;
|
||||
use crate::error::BotError::{AccountDoesNotExist, AuthenticationError, PasswordCreationError};
|
||||
use crate::logic::{hash_password, record_room_information};
|
||||
use crate::models::User;
|
||||
use crate::logic::record_room_information;
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk::identifiers::UserId;
|
||||
|
||||
|
@ -36,74 +33,3 @@ impl Command for ResyncCommand {
|
|||
Execution::success(message)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RegisterCommand(pub String);
|
||||
|
||||
#[async_trait]
|
||||
impl Command for RegisterCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"register user account"
|
||||
}
|
||||
|
||||
fn is_secure(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
async fn execute(&self, ctx: &Context<'_>) -> ExecutionResult {
|
||||
let pw_hash = hash_password(&self.0).map_err(|e| PasswordCreationError(e))?;
|
||||
let user = User {
|
||||
username: ctx.username.to_owned(),
|
||||
password: pw_hash,
|
||||
};
|
||||
|
||||
ctx.db.upsert_user(&user).await?;
|
||||
Execution::success("User account registered/updated".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CheckCommand(pub String);
|
||||
|
||||
#[async_trait]
|
||||
impl Command for CheckCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"check user password"
|
||||
}
|
||||
|
||||
fn is_secure(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
async fn execute(&self, ctx: &Context<'_>) -> ExecutionResult {
|
||||
let user = ctx.db.authenticate_user(&ctx.username, &self.0).await?;
|
||||
|
||||
match user {
|
||||
Some(_) => Execution::success("Password is correct!".to_string()),
|
||||
None => Err(AuthenticationError.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UnregisterCommand;
|
||||
|
||||
#[async_trait]
|
||||
impl Command for UnregisterCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"unregister user account"
|
||||
}
|
||||
|
||||
fn is_secure(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
async fn execute(&self, ctx: &Context<'_>) -> ExecutionResult {
|
||||
let user = ctx.db.get_user(&ctx.username).await?;
|
||||
|
||||
match user {
|
||||
Some(_) => {
|
||||
ctx.db.delete_user(&ctx.username).await?;
|
||||
Execution::success("Your user account has been removed.".to_string())
|
||||
}
|
||||
None => Err(AccountDoesNotExist.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
use crate::context::Context;
|
||||
use crate::error::BotError;
|
||||
use async_trait::async_trait;
|
||||
use log::{error, info};
|
||||
use thiserror::Error;
|
||||
use BotError::DataError;
|
||||
|
||||
|
@ -20,9 +19,6 @@ pub enum CommandError {
|
|||
#[error("invalid command: {0}")]
|
||||
InvalidCommand(String),
|
||||
|
||||
#[error("command can only be executed from encrypted direct message")]
|
||||
InsecureExecution,
|
||||
|
||||
#[error("ignored command")]
|
||||
IgnoredCommand,
|
||||
}
|
||||
|
@ -103,72 +99,18 @@ pub trait Command: Send + Sync {
|
|||
fn is_secure(&self) -> bool;
|
||||
}
|
||||
|
||||
/// Determine if we are allowed to execute this command. Currently the
|
||||
/// rules are that secure commands must be executed in secure rooms
|
||||
/// (encrypted + direct), and anything else can be executed where
|
||||
/// ever. Later, we can add stuff like admin/regular user power
|
||||
/// separation, etc.
|
||||
fn execution_allowed(cmd: &(impl Command + ?Sized), ctx: &Context<'_>) -> Result<(), CommandError> {
|
||||
if cmd.is_secure() {
|
||||
if ctx.is_secure() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(CommandError::InsecureExecution)
|
||||
}
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to execute a command, and return the content that should
|
||||
/// go back to Matrix, if the command was executed (successfully or
|
||||
/// not). If a command is determined to be ignored, this function will
|
||||
/// return None, signifying that we should not send a response.
|
||||
pub async fn execute_command(ctx: &Context<'_>) -> ExecutionResult {
|
||||
let cmd = parser::parse_command(&ctx.message_body)?;
|
||||
|
||||
let result = match execution_allowed(cmd.as_ref(), ctx) {
|
||||
Ok(_) => cmd.execute(ctx).await,
|
||||
Err(e) => Err(ExecutionError(e.into())),
|
||||
};
|
||||
|
||||
log_command(cmd.as_ref(), ctx, &result);
|
||||
result
|
||||
}
|
||||
|
||||
/// Log result of an executed command.
|
||||
fn log_command(cmd: &(impl Command + ?Sized), ctx: &Context, result: &ExecutionResult) {
|
||||
use substring::Substring;
|
||||
let command = match cmd.is_secure() {
|
||||
true => cmd.name(),
|
||||
false => ctx.message_body.substring(0, 30),
|
||||
};
|
||||
|
||||
let dots = match ctx.message_body.len() {
|
||||
_len if _len > 30 => "[...]",
|
||||
_ => "",
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
info!(
|
||||
"[{}] {} <{}{}> - success",
|
||||
ctx.room.display_name, ctx.username, command, dots
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
"[{}] {} <{}{}> - {}",
|
||||
ctx.room.display_name, ctx.username, command, dots, e
|
||||
);
|
||||
}
|
||||
};
|
||||
cmd.execute(ctx).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use management::RegisterCommand;
|
||||
use url::Url;
|
||||
|
||||
macro_rules! dummy_room {
|
||||
|
@ -181,100 +123,6 @@ mod tests {
|
|||
};
|
||||
}
|
||||
|
||||
macro_rules! secure_room {
|
||||
() => {
|
||||
crate::context::RoomContext {
|
||||
id: &matrix_sdk::identifiers::room_id!("!fakeroomid:example.com"),
|
||||
display_name: "displayname".to_owned(),
|
||||
secure: true,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
async fn secure_context_secure_command_allows_execution() {
|
||||
let db_path = tempfile::NamedTempFile::new_in(".").unwrap();
|
||||
let db = crate::db::sqlite::Database::new(db_path.path().to_str().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let homeserver = Url::parse("http://example.com").unwrap();
|
||||
|
||||
let ctx = Context {
|
||||
db: db,
|
||||
matrix_client: &matrix_sdk::Client::new(homeserver).unwrap(),
|
||||
room: secure_room!(),
|
||||
username: "myusername",
|
||||
message_body: "!notacommand",
|
||||
};
|
||||
|
||||
let cmd = RegisterCommand("".to_owned());
|
||||
assert_eq!(execution_allowed(&cmd, &ctx).is_ok(), true);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
async fn secure_context_insecure_command_allows_execution() {
|
||||
let db_path = tempfile::NamedTempFile::new_in(".").unwrap();
|
||||
let db = crate::db::sqlite::Database::new(db_path.path().to_str().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let homeserver = Url::parse("http://example.com").unwrap();
|
||||
|
||||
let ctx = Context {
|
||||
db: db,
|
||||
matrix_client: &matrix_sdk::Client::new(homeserver).unwrap(),
|
||||
room: secure_room!(),
|
||||
username: "myusername",
|
||||
message_body: "!notacommand",
|
||||
};
|
||||
|
||||
let cmd = variables::GetVariableCommand("".to_owned());
|
||||
assert_eq!(execution_allowed(&cmd, &ctx).is_ok(), true);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
async fn insecure_context_insecure_command_allows_execution() {
|
||||
let db_path = tempfile::NamedTempFile::new_in(".").unwrap();
|
||||
let db = crate::db::sqlite::Database::new(db_path.path().to_str().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let homeserver = Url::parse("http://example.com").unwrap();
|
||||
|
||||
let ctx = Context {
|
||||
db: db,
|
||||
matrix_client: &matrix_sdk::Client::new(homeserver).unwrap(),
|
||||
room: dummy_room!(),
|
||||
username: "myusername",
|
||||
message_body: "!notacommand",
|
||||
};
|
||||
|
||||
let cmd = variables::GetVariableCommand("".to_owned());
|
||||
assert_eq!(execution_allowed(&cmd, &ctx).is_ok(), true);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
async fn insecure_context_secure_command_denies_execution() {
|
||||
let db_path = tempfile::NamedTempFile::new_in(".").unwrap();
|
||||
let db = crate::db::sqlite::Database::new(db_path.path().to_str().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let homeserver = Url::parse("http://example.com").unwrap();
|
||||
|
||||
let ctx = Context {
|
||||
db: db,
|
||||
matrix_client: &matrix_sdk::Client::new(homeserver).unwrap(),
|
||||
room: dummy_room!(),
|
||||
username: "myusername",
|
||||
message_body: "!notacommand",
|
||||
};
|
||||
|
||||
let cmd = RegisterCommand("".to_owned());
|
||||
assert_eq!(execution_allowed(&cmd, &ctx).is_err(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_result_extractor_creates_bubble() {
|
||||
let result = Execution::success("test".to_string());
|
||||
|
@ -300,7 +148,6 @@ mod tests {
|
|||
username: "myusername",
|
||||
message_body: "!notacommand",
|
||||
};
|
||||
|
||||
let result = execute_command(&ctx).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ use crate::commands::{
|
|||
basic_rolling::RollCommand,
|
||||
cofd::PoolRollCommand,
|
||||
cthulhu::{CthAdvanceRoll, CthRoll},
|
||||
management::{CheckCommand, RegisterCommand, ResyncCommand, UnregisterCommand},
|
||||
management::ResyncCommand,
|
||||
misc::HelpCommand,
|
||||
variables::{
|
||||
DeleteVariableCommand, GetAllVariablesCommand, GetVariableCommand, SetVariableCommand,
|
||||
|
@ -47,18 +47,6 @@ fn parse_roll(input: &str) -> Result<Box<dyn Command>, BotError> {
|
|||
}
|
||||
}
|
||||
|
||||
fn parse_register_command(input: &str) -> Result<Box<dyn Command>, BotError> {
|
||||
Ok(Box::new(RegisterCommand(input.to_owned())))
|
||||
}
|
||||
|
||||
fn parse_check_command(input: &str) -> Result<Box<dyn Command>, BotError> {
|
||||
Ok(Box::new(CheckCommand(input.to_owned())))
|
||||
}
|
||||
|
||||
fn parse_unregister_command() -> Result<Box<dyn Command>, BotError> {
|
||||
Ok(Box::new(UnregisterCommand))
|
||||
}
|
||||
|
||||
fn parse_get_variable_command(input: &str) -> Result<Box<dyn Command>, BotError> {
|
||||
Ok(Box::new(GetVariableCommand(input.to_owned())))
|
||||
}
|
||||
|
@ -153,9 +141,6 @@ pub fn parse_command(input: &str) -> Result<Box<dyn Command>, BotError> {
|
|||
"cthadv" | "ctharoll" => parse_cth_advancement_roll(&cmd_input),
|
||||
"chance" => chance_die(),
|
||||
"help" => help(&cmd_input),
|
||||
"register" => parse_register_command(&cmd_input),
|
||||
"check" => parse_check_command(&cmd_input),
|
||||
"unregister" => parse_unregister_command(),
|
||||
_ => Err(CommandParsingError::UnrecognizedCommand(cmd).into()),
|
||||
},
|
||||
//All other errors passed up.
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
use crate::error::BotError;
|
||||
use crate::models::User;
|
||||
use async_trait::async_trait;
|
||||
use errors::DataError;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
@ -16,21 +14,6 @@ pub(crate) trait DbState {
|
|||
async fn set_device_id(&self, device_id: &str) -> Result<(), DataError>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub(crate) trait Users {
|
||||
async fn upsert_user(&self, user: &User) -> Result<(), DataError>;
|
||||
|
||||
async fn get_user(&self, username: &str) -> Result<Option<User>, DataError>;
|
||||
|
||||
async fn delete_user(&self, username: &str) -> Result<(), DataError>;
|
||||
|
||||
async fn authenticate_user(
|
||||
&self,
|
||||
username: &str,
|
||||
raw_password: &str,
|
||||
) -> Result<Option<User>, BotError>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub(crate) trait Rooms {
|
||||
async fn should_process(&self, room_id: &str, event_id: &str) -> Result<bool, DataError>;
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
use barrel::backend::Sqlite;
|
||||
use barrel::{types, types::Type, Migration};
|
||||
|
||||
fn primary_uuid() -> Type {
|
||||
types::text().unique(true).primary(true).nullable(false)
|
||||
}
|
||||
|
||||
pub fn migration() -> String {
|
||||
let mut m = Migration::new();
|
||||
|
||||
//Table of room ID, event ID, event timestamp
|
||||
m.create_table("accounts", move |t| {
|
||||
t.add_column("user_id", primary_uuid());
|
||||
t.add_column("password", types::text().nullable(false));
|
||||
});
|
||||
|
||||
m.make::<Sqlite>()
|
||||
}
|
|
@ -7,7 +7,6 @@ use std::str::FromStr;
|
|||
pub mod migrator;
|
||||
pub mod rooms;
|
||||
pub mod state;
|
||||
pub mod users;
|
||||
pub mod variables;
|
||||
|
||||
pub struct Database {
|
||||
|
|
|
@ -1,210 +0,0 @@
|
|||
use super::Database;
|
||||
use crate::db::{errors::DataError, Users};
|
||||
use crate::error::BotError;
|
||||
use crate::models::User;
|
||||
use async_trait::async_trait;
|
||||
|
||||
#[async_trait]
|
||||
impl Users for Database {
|
||||
async fn upsert_user(&self, user: &User) -> Result<(), DataError> {
|
||||
sqlx::query(
|
||||
r#"INSERT INTO accounts (user_id, password) VALUES (?, ?)
|
||||
ON CONFLICT(user_id) DO UPDATE SET password = ?"#,
|
||||
)
|
||||
.bind(&user.username)
|
||||
.bind(&user.password)
|
||||
.bind(&user.password)
|
||||
.execute(&self.conn)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_user(&self, username: &str) -> Result<(), DataError> {
|
||||
sqlx::query(r#"DELETE FROM accounts WHERE user_id = ?"#)
|
||||
.bind(&username)
|
||||
.execute(&self.conn)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_user(&self, username: &str) -> Result<Option<User>, DataError> {
|
||||
let user_row = sqlx::query!(
|
||||
r#"SELECT user_id, password FROM accounts
|
||||
WHERE user_id = ?"#,
|
||||
username
|
||||
)
|
||||
.fetch_optional(&self.conn)
|
||||
.await?;
|
||||
|
||||
Ok(user_row.map(|u| User {
|
||||
username: u.user_id,
|
||||
password: u.password,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn authenticate_user(
|
||||
&self,
|
||||
username: &str,
|
||||
raw_password: &str,
|
||||
) -> Result<Option<User>, BotError> {
|
||||
let user = self.get_user(username).await?;
|
||||
Ok(user.filter(|u| u.verify_password(raw_password)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::db::sqlite::Database;
|
||||
use crate::db::Users;
|
||||
|
||||
async fn create_db() -> Database {
|
||||
let db_path = tempfile::NamedTempFile::new_in(".").unwrap();
|
||||
crate::db::sqlite::migrator::migrate(db_path.path().to_str().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Database::new(db_path.path().to_str().unwrap())
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
async fn create_and_get_user_test() {
|
||||
let db = create_db().await;
|
||||
|
||||
let insert_result = db
|
||||
.upsert_user(&User {
|
||||
username: "myuser".to_string(),
|
||||
password: "abc".to_string(),
|
||||
})
|
||||
.await;
|
||||
|
||||
assert!(insert_result.is_ok());
|
||||
|
||||
let user = db
|
||||
.get_user("myuser")
|
||||
.await
|
||||
.expect("User retrieval query failed");
|
||||
|
||||
assert!(user.is_some());
|
||||
let user = user.unwrap();
|
||||
assert_eq!(user.username, "myuser");
|
||||
assert_eq!(user.password, "abc");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
async fn can_update_user() {
|
||||
let db = create_db().await;
|
||||
|
||||
let insert_result1 = db
|
||||
.upsert_user(&User {
|
||||
username: "myuser".to_string(),
|
||||
password: "abc".to_string(),
|
||||
})
|
||||
.await;
|
||||
|
||||
assert!(insert_result1.is_ok());
|
||||
|
||||
let insert_result2 = db
|
||||
.upsert_user(&User {
|
||||
username: "myuser".to_string(),
|
||||
password: "123".to_string(),
|
||||
})
|
||||
.await;
|
||||
|
||||
assert!(insert_result2.is_ok());
|
||||
|
||||
let user = db
|
||||
.get_user("myuser")
|
||||
.await
|
||||
.expect("User retrieval query failed");
|
||||
|
||||
assert!(user.is_some());
|
||||
let user = user.unwrap();
|
||||
assert_eq!(user.username, "myuser");
|
||||
assert_eq!(user.password, "123"); //From second upsert
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
async fn can_delete_user() {
|
||||
let db = create_db().await;
|
||||
|
||||
let insert_result = db
|
||||
.upsert_user(&User {
|
||||
username: "myuser".to_string(),
|
||||
password: "abc".to_string(),
|
||||
})
|
||||
.await;
|
||||
|
||||
assert!(insert_result.is_ok());
|
||||
|
||||
db.delete_user("myuser")
|
||||
.await
|
||||
.expect("User deletion query failed");
|
||||
|
||||
let user = db
|
||||
.get_user("myuser")
|
||||
.await
|
||||
.expect("User retrieval query failed");
|
||||
|
||||
assert!(user.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
async fn username_not_in_db_returns_none() {
|
||||
let db = create_db().await;
|
||||
let user = db
|
||||
.get_user("does not exist")
|
||||
.await
|
||||
.expect("Get user query failure");
|
||||
|
||||
assert!(user.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
async fn authenticate_user_is_some_with_valid_password() {
|
||||
let db = create_db().await;
|
||||
|
||||
let insert_result = db
|
||||
.upsert_user(&User {
|
||||
username: "myuser".to_string(),
|
||||
password: crate::logic::hash_password("abc").expect("password hash error!"),
|
||||
})
|
||||
.await;
|
||||
|
||||
assert!(insert_result.is_ok());
|
||||
|
||||
let user = db
|
||||
.authenticate_user("myuser", "abc")
|
||||
.await
|
||||
.expect("User retrieval query failed");
|
||||
|
||||
assert!(user.is_some());
|
||||
let user = user.unwrap();
|
||||
assert_eq!(user.username, "myuser");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
async fn authenticate_user_is_none_with_wrong_password() {
|
||||
let db = create_db().await;
|
||||
|
||||
let insert_result = db
|
||||
.upsert_user(&User {
|
||||
username: "myuser".to_string(),
|
||||
password: crate::logic::hash_password("abc").expect("password hash error!"),
|
||||
})
|
||||
.await;
|
||||
|
||||
assert!(insert_result.is_ok());
|
||||
|
||||
let user = db
|
||||
.authenticate_user("myuser", "wrong-password")
|
||||
.await
|
||||
.expect("User retrieval query failed");
|
||||
|
||||
assert!(user.is_none());
|
||||
}
|
||||
}
|
|
@ -78,15 +78,6 @@ pub enum BotError {
|
|||
|
||||
#[error("identifier error: {0}")]
|
||||
IdentifierError(#[from] matrix_sdk::identifiers::Error),
|
||||
|
||||
#[error("password creation error: {0}")]
|
||||
PasswordCreationError(argon2::Error),
|
||||
|
||||
#[error("account does not exist, or password incorrect")]
|
||||
AuthenticationError,
|
||||
|
||||
#[error("user account does not exist, try registering")]
|
||||
AccountDoesNotExist,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
|
|
|
@ -4,10 +4,8 @@ use crate::error::{BotError, DiceRollingError};
|
|||
use crate::matrix;
|
||||
use crate::models::RoomInfo;
|
||||
use crate::parser::dice::{Amount, Element};
|
||||
use argon2::{self, Config, Error as ArgonError};
|
||||
use futures::stream::{self, StreamExt, TryStreamExt};
|
||||
use matrix_sdk::{self, identifiers::RoomId, Client};
|
||||
use rand::Rng;
|
||||
use std::slice;
|
||||
|
||||
/// Record the information about a room, including users in it.
|
||||
|
@ -88,10 +86,3 @@ pub async fn calculate_dice_amount(amounts: &[Amount], ctx: &Context<'_>) -> Res
|
|||
|
||||
Ok(dice_amount)
|
||||
}
|
||||
|
||||
/// Hash a password using the argon2 algorithm with a 16 byte salt.
|
||||
pub(crate) fn hash_password(raw_password: &str) -> Result<String, ArgonError> {
|
||||
let salt = rand::thread_rng().gen::<[u8; 16]>();
|
||||
let config = Config::default();
|
||||
argon2::hash_encoded(raw_password.as_bytes(), &salt, &config)
|
||||
}
|
||||
|
|
|
@ -6,40 +6,3 @@ pub struct RoomInfo {
|
|||
pub room_id: String,
|
||||
pub room_name: String,
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Debug)]
|
||||
pub struct User {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub fn verify_password(&self, raw_password: &str) -> bool {
|
||||
argon2::verify_encoded(&self.password, raw_password.as_bytes()).unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn verify_password_passes_with_correct_password() {
|
||||
let user = User {
|
||||
username: "myuser".to_string(),
|
||||
password: crate::logic::hash_password("mypassword").expect("Password hashing error!"),
|
||||
};
|
||||
|
||||
assert_eq!(user.verify_password("mypassword"), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_password_fails_with_wrong_password() {
|
||||
let user = User {
|
||||
username: "myuser".to_string(),
|
||||
password: crate::logic::hash_password("mypassword").expect("Password hashing error!"),
|
||||
};
|
||||
|
||||
assert_eq!(user.verify_password("wrong-password"), false);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue