Functional user account registration.

This commit is contained in:
projectmoon 2021-05-22 14:52:32 +00:00
parent c1ec7366e4
commit ca34841d86
9 changed files with 191 additions and 2 deletions

36
Cargo.lock generated
View File

@ -101,6 +101,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "arrayref"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544"
[[package]] [[package]]
name = "arrayvec" name = "arrayvec"
version = "0.5.2" version = "0.5.2"
@ -198,6 +204,17 @@ dependencies = [
"wyz", "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]] [[package]]
name = "block-buffer" name = "block-buffer"
version = "0.9.0" version = "0.9.0"
@ -313,6 +330,12 @@ version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f92cfa0fd5690b3cf8c1ef2cabbd9b7ef22fa53cf5e1f92b05103f6d5d1cf6e7" checksum = "f92cfa0fd5690b3cf8c1ef2cabbd9b7ef22fa53cf5e1f92b05103f6d5d1cf6e7"
[[package]]
name = "constant_time_eq"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
[[package]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.9.1" version = "0.9.1"
@ -2025,6 +2048,18 @@ dependencies = [
"smallvec", "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]] [[package]]
name = "rustc_version" name = "rustc_version"
version = "0.2.3" version = "0.2.3"
@ -2510,6 +2545,7 @@ dependencies = [
"phf", "phf",
"rand 0.8.3", "rand 0.8.3",
"refinery", "refinery",
"rust-argon2",
"serde", "serde",
"sqlx", "sqlx",
"tempfile", "tempfile",

View File

@ -16,6 +16,7 @@ tracing-subscriber = "0.2"
toml = "0.5" toml = "0.5"
nom = "5" nom = "5"
rand = "0.8" rand = "0.8"
rust-argon2 = "0.8"
thiserror = "1.0" thiserror = "1.0"
itertools = "0.10" itertools = "0.10"
async-trait = "0.1" async-trait = "0.1"

View File

@ -1,6 +1,9 @@
use super::{Command, Execution, ExecutionResult}; use super::{Command, Execution, ExecutionResult};
use crate::context::Context; use crate::context::Context;
use crate::logic::record_room_information; use crate::db::Users;
use crate::error::BotError::PasswordCreationError;
use crate::logic::{hash_password, record_room_information};
use crate::models::User;
use async_trait::async_trait; use async_trait::async_trait;
use matrix_sdk::identifiers::UserId; use matrix_sdk::identifiers::UserId;
@ -47,6 +50,13 @@ impl Command for RegisterCommand {
} }
async fn execute(&self, ctx: &Context<'_>) -> ExecutionResult { async fn execute(&self, ctx: &Context<'_>) -> ExecutionResult {
Execution::success("User account registered".to_string()) 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())
} }
} }

View File

@ -1,3 +1,5 @@
use crate::error::BotError;
use crate::models::User;
use async_trait::async_trait; use async_trait::async_trait;
use errors::DataError; use errors::DataError;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
@ -14,6 +16,19 @@ pub(crate) trait DbState {
async fn set_device_id(&self, device_id: &str) -> Result<(), DataError>; 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 authenticate_user(
&self,
username: &str,
raw_password: &str,
) -> Result<Option<User>, BotError>;
}
#[async_trait] #[async_trait]
pub(crate) trait Rooms { pub(crate) trait Rooms {
async fn should_process(&self, room_id: &str, event_id: &str) -> Result<bool, DataError>; async fn should_process(&self, room_id: &str, event_id: &str) -> Result<bool, DataError>;

View File

@ -7,6 +7,7 @@ use std::str::FromStr;
pub mod migrator; pub mod migrator;
pub mod rooms; pub mod rooms;
pub mod state; pub mod state;
pub mod users;
pub mod variables; pub mod variables;
pub struct Database { pub struct Database {

102
src/db/sqlite/users.rs Normal file
View File

@ -0,0 +1,102 @@
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 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 crate::db::sqlite::Database;
use crate::db::DbState;
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 set_and_get_device_id() {
let db = create_db().await;
db.set_device_id("device_id")
.await
.expect("Could not set device ID");
let device_id = db.get_device_id().await.expect("Could not get device ID");
assert!(device_id.is_some());
assert_eq!(device_id.unwrap(), "device_id");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn no_device_id_set_returns_none() {
let db = create_db().await;
let device_id = db.get_device_id().await.expect("Could not get device ID");
assert!(device_id.is_none());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn can_update_device_id() {
let db = create_db().await;
db.set_device_id("device_id")
.await
.expect("Could not set device ID");
db.set_device_id("device_id2")
.await
.expect("Could not set device ID");
let device_id = db.get_device_id().await.expect("Could not get device ID");
assert!(device_id.is_some());
assert_eq!(device_id.unwrap(), "device_id2");
}
}

View File

@ -78,6 +78,9 @@ pub enum BotError {
#[error("identifier error: {0}")] #[error("identifier error: {0}")]
IdentifierError(#[from] matrix_sdk::identifiers::Error), IdentifierError(#[from] matrix_sdk::identifiers::Error),
#[error("password creation error: {0}")]
PasswordCreationError(argon2::Error),
} }
#[derive(Error, Debug)] #[derive(Error, Debug)]

View File

@ -4,8 +4,10 @@ use crate::error::{BotError, DiceRollingError};
use crate::matrix; use crate::matrix;
use crate::models::RoomInfo; use crate::models::RoomInfo;
use crate::parser::dice::{Amount, Element}; use crate::parser::dice::{Amount, Element};
use argon2::{self, Config, Error as ArgonError};
use futures::stream::{self, StreamExt, TryStreamExt}; use futures::stream::{self, StreamExt, TryStreamExt};
use matrix_sdk::{self, identifiers::RoomId, Client}; use matrix_sdk::{self, identifiers::RoomId, Client};
use rand::Rng;
use std::slice; use std::slice;
/// Record the information about a room, including users in it. /// Record the information about a room, including users in it.
@ -86,3 +88,10 @@ pub async fn calculate_dice_amount(amounts: &[Amount], ctx: &Context<'_>) -> Res
Ok(dice_amount) 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)
}

View File

@ -6,3 +6,15 @@ pub struct RoomInfo {
pub room_id: String, pub room_id: String,
pub room_name: 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)
}
}